Compare commits
176 Commits
fcf9a065e1
...
v0.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
eac8fa6edb
|
|||
|
43f918a074
|
|||
| e322867d79 | |||
|
4d6fa318b7
|
|||
| 4e8878a4b5 | |||
|
e65b890880
|
|||
|
f57edd4d3b
|
|||
|
1afd56fb80
|
|||
| 71669a4b96 | |||
|
c312e30c17
|
|||
| 51f4556ede | |||
| 4fa4ae6b54 | |||
| 869ff4691b | |||
|
822a2dc018
|
|||
|
5b7fc3707b
|
|||
| 0e2dc54dc6 | |||
| 87f09c94d0 | |||
|
b33b8104a8
|
|||
| 4a4a222973 | |||
| 8c524abcf5 | |||
|
a852ab75ae
|
|||
|
de1f234c15
|
|||
| 4581900427 | |||
|
56d91083e5
|
|||
|
ba7c3795f8
|
|||
|
bbf3fb91a0
|
|||
| 1754df73cb | |||
|
9a1f9abf84
|
|||
|
2753388e1e
|
|||
|
f3159d30f1
|
|||
|
ca238be6f4
|
|||
|
8747ce4eb0
|
|||
|
fcda3b9c8c
|
|||
|
67689dcce3
|
|||
|
22ffcd54db
|
|||
|
bd1b177993
|
|||
|
3f110995a4
|
|||
|
a7410058fa
|
|||
|
411587456b
|
|||
|
84e915ece9
|
|||
|
70ac3b0a70
|
|||
|
a7cbd8ce36
|
|||
|
c9052b35f6
|
|||
|
3b96130491
|
|||
|
176b1a10c6
|
|||
|
1c54e4c0b5
|
|||
|
7796a22491
|
|||
|
7e6e917ae1
|
|||
|
28cfe4b1e7
|
|||
|
179a82d2dd
|
|||
|
420442c1c0
|
|||
|
68c5758ecc
|
|||
|
c5dd3c30a6
|
|||
|
422d5c7cd2
|
|||
|
5a23d523a8
|
|||
|
f8da034e66
|
|||
|
b0b56fcf92
|
|||
| 0cf000c1b8 | |||
| fa9a924b0a | |||
|
50f91cc7d7
|
|||
|
a628a03f84
|
|||
|
eaf41e0835
|
|||
|
243cf9c08d
|
|||
|
c32fc51aab
|
|||
|
aa9178d569
|
|||
|
281938dd64
|
|||
|
fafc5d8f6f
|
|||
|
1238359b5f
|
|||
| 84220beb1c | |||
|
1e9ec9bb76
|
|||
| 21e51a7c40 | |||
|
e3c30f7b16
|
|||
|
b4f0c60ea0
|
|||
|
1a5a2177b4
|
|||
|
7e8443c598
|
|||
|
7b71f2cf76
|
|||
|
c7b137e5eb
|
|||
|
958d18d61a
|
|||
|
3aa0c49507
|
|||
|
|
4e566a0607 | ||
|
|
aab6793b86
|
||
|
|
cfd0935bdc
|
||
|
|
c2dae105ff
|
||
|
|
2a70bf2fb9
|
||
|
|
9a9947f9ad
|
||
|
|
bdf5a18ad4
|
||
|
|
aa399b862a
|
||
|
|
713e91a720
|
||
|
|
8ec2a6d7e4
|
||
|
|
4ecf2c4246
|
||
|
|
4fdf8accd6
|
||
|
|
f451adcb53
|
||
|
|
721dccb499
|
||
|
|
27bb7d1bfe
|
||
|
|
1d44181fb5
|
||
|
|
de67f59d5c
|
||
|
|
1995e6dda2
|
||
|
|
600cfe0f78
|
||
|
|
e301ac8e2e
|
||
|
|
03a1d9f277
|
||
|
|
00049f3743
|
||
|
|
60c0a43f33
|
||
|
|
0c1b1b4afe
|
||
|
|
92310d434a
|
||
|
|
56c127ca0c
|
||
|
|
5075fef616
|
||
|
|
8e090daa9c
|
||
|
|
def87a1621
|
||
|
|
00ec7fa21c
|
||
|
|
2b8bfaaca8
|
||
|
|
3e9a08a266
|
||
|
|
fcea11f0e5
|
||
|
|
261a782963
|
||
|
|
e964e7e52c
|
||
|
|
e508407df4
|
||
|
|
bec827acb1
|
||
|
|
0a69603643
|
||
|
|
d4f71e98ed
|
||
|
|
e56c9bd0d5
|
||
|
|
e1b7e1b2ef
|
||
|
|
1056ffd08e
|
||
| be5fe00f20 | |||
|
|
e9c4929726
|
||
| 14ff0c0e16 | |||
|
|
d939f5d649
|
||
|
|
69fffb29d8
|
||
|
|
91d3b977e9
|
||
| 7a5fd46835 | |||
|
|
9c4c5c2553
|
||
|
|
8f819d12c0
|
||
|
|
b810e27480
|
||
|
|
1949f1876f
|
||
|
|
2ba0116ca6
|
||
|
|
2c2ddabdff
|
||
|
|
dfcdbec0dd
|
||
|
|
3b67a8791c
|
||
|
|
d5ab532947
|
||
|
|
50c63d5c38
|
||
|
|
64d09cfb7f
|
||
|
|
def44618ef
|
||
|
|
9e5aeaf572
|
||
|
|
86f85a90f4
|
||
| d8a35ac3fd | |||
|
|
5a5f62e98a
|
||
|
|
074f9afcbb
|
||
|
|
725fd2e5ea
|
||
|
|
8349ca5e12
|
||
|
|
46d59e3371
|
||
|
|
e8e6ee0bc4
|
||
|
|
a91ee2bd0a
|
||
|
|
fcb6923c92
|
||
|
|
0f3b9f176e
|
||
| 822ae2f945 | |||
|
|
96c669ab4e
|
||
|
|
558100c35e
|
||
|
|
6739b38f4c
|
||
| 7e1272c936 | |||
|
|
ecdeb4c122
|
||
|
|
8614e2f12b
|
||
|
|
a038a857d9
|
||
|
|
eee81d0cf1
|
||
|
|
b7fa4b012a
|
||
|
|
10bcd5c32b
|
||
|
|
f79d5d4724
|
||
|
|
866ffbe615
|
||
|
|
3c1fe3396d
|
||
|
|
e4242333d9
|
||
|
|
138f13c1a0
|
||
|
|
ad5e515200
|
||
|
|
1ea8b22a59
|
||
|
|
f49aff262c
|
||
| 852e2fea1e | |||
|
|
353b55fe1a
|
||
|
|
ba0cbba96b
|
||
|
|
5f921f1b53
|
||
|
|
a2d27bf575
|
@@ -17,7 +17,7 @@ steps:
|
||||
branch:
|
||||
- master
|
||||
- name: rspec
|
||||
image: guildeducation/rails:2.7.2-14.20.0
|
||||
image: gitea.kosmos.org/kosmos/akkounts-ci:0.9.1
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
@@ -28,6 +28,8 @@ steps:
|
||||
- bundle config set cache_path 'vendor/cache'
|
||||
- bundle config set with 'development test'
|
||||
- bundle install --jobs=3 --retry=3
|
||||
- bundle exec rails db:create
|
||||
- bundle exec rails db:migrate
|
||||
- yarn install
|
||||
- rake css:build
|
||||
- bundle exec rspec
|
||||
|
||||
90
.env.example
@@ -1,44 +1,64 @@
|
||||
PRIMARY_DOMAIN=kosmos.org
|
||||
AKKOUNTS_DOMAIN=accounts.example.com
|
||||
# PRIMARY_DOMAIN=kosmos.org
|
||||
# AKKOUNTS_DOMAIN=accounts.example.com
|
||||
|
||||
SMTP_SERVER=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=accounts
|
||||
SMTP_PASSWORD=123abc
|
||||
SMTP_FROM_ADDRESS=accounts@example.com
|
||||
SMTP_DOMAIN=example.com
|
||||
SMTP_AUTH_METHOD=plain
|
||||
SMTP_ENABLE_STARTTLS=auto
|
||||
# SMTP_SERVER=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_LOGIN=accounts
|
||||
# SMTP_PASSWORD=123abc
|
||||
# SMTP_FROM_ADDRESS=accounts@example.com
|
||||
# SMTP_DOMAIN=example.com
|
||||
# SMTP_AUTH_METHOD=plain
|
||||
# SMTP_ENABLE_STARTTLS=auto
|
||||
|
||||
LDAP_HOST=localhost
|
||||
LDAP_PORT=389
|
||||
LDAP_ADMIN_PASSWORD=passthebutter
|
||||
LDAP_SUFFIX='dc=kosmos,dc=org'
|
||||
# S3_ENABLED=true
|
||||
# S3_ENDPOINT=https://s3.kosmos.org
|
||||
# S3_REGION=garage
|
||||
# S3_BUCKET=akkounts-production
|
||||
# S3_ALIAS_HOST=https://accounts.web.s3.kosmos.org
|
||||
# S3_ACCESS_KEY=123456abcdefg
|
||||
# S3_SECRET_KEY=123456789123456789123456789
|
||||
|
||||
REDIS_URL='redis://localhost:6379/1'
|
||||
# LDAP_HOST=localhost
|
||||
# LDAP_PORT=389
|
||||
# LDAP_ADMIN_PASSWORD=passthebutter
|
||||
# LDAP_SUFFIX='dc=kosmos,dc=org'
|
||||
|
||||
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||
# REDIS_URL='redis://localhost:6379/1'
|
||||
|
||||
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||
# WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||
|
||||
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||
MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
RS_REDIS_URL='redis://localhost:6379/2'
|
||||
#
|
||||
# Service Integrations
|
||||
#
|
||||
|
||||
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
# BTCPAY_STORE_ID=''
|
||||
# BTCPAY_AUTH_TOKEN=''
|
||||
|
||||
BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||
|
||||
LNDHUB_API_URL='http://localhost:3023'
|
||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||
LNDHUB_ADMIN_UI=true
|
||||
LNDHUB_PG_HOST=localhost
|
||||
LNDHUB_PG_PORT=5432
|
||||
LNDHUB_PG_DATABASE=lndhub
|
||||
LNDHUB_PG_USERNAME=lndhub
|
||||
LNDHUB_PG_PASSWORD=''
|
||||
# DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
||||
|
||||
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||
# EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||
|
||||
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||
|
||||
# LNDHUB_API_URL='http://localhost:3023'
|
||||
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
# LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||
# LNDHUB_ADMIN_UI=true
|
||||
# LNDHUB_ADMIN_TOKEN=123456789
|
||||
# LNDHUB_PG_HOST=localhost
|
||||
# LNDHUB_PG_PORT=5432
|
||||
# LNDHUB_PG_DATABASE=lndhub
|
||||
# LNDHUB_PG_USERNAME=lndhub
|
||||
# LNDHUB_PG_PASSWORD=''
|
||||
|
||||
# MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||
|
||||
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||
|
||||
# RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
# RS_REDIS_URL='redis://localhost:6379/2'
|
||||
|
||||
@@ -2,13 +2,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_CONNECT_SECRET='discourse_connect_ftw'
|
||||
|
||||
EJABBERD_API_URL='http://xmpp.example.com/api'
|
||||
|
||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||
|
||||
LNDHUB_API_URL='http://localhost:3026'
|
||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||
|
||||
2
.gitignore
vendored
@@ -23,6 +23,7 @@
|
||||
!/tmp/pids/
|
||||
!/tmp/pids/.keep
|
||||
|
||||
/storage
|
||||
|
||||
/public/assets
|
||||
.byebug_history
|
||||
@@ -39,6 +40,7 @@ yarn-debug.log*
|
||||
|
||||
# Ignore local dotenv config file
|
||||
.env
|
||||
.env.development
|
||||
|
||||
# Ignore redis dumps from sidekiq
|
||||
dump.rdb
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.7.2
|
||||
3.3.0
|
||||
|
||||
14
Dockerfile
@@ -1,10 +1,18 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ruby:2.7.6
|
||||
FROM debian:bullseye-slim as base
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
|
||||
ldap-utils tini
|
||||
# TODO Remove when upstream Ruby works properly on Apple silicon
|
||||
RUN apt update && apt install -y build-essential wget autoconf libpq-dev pkg-config
|
||||
RUN wget https://github.com/postmodern/ruby-install/releases/download/v0.9.3/ruby-install-0.9.3.tar.gz \
|
||||
&& tar -xzvf ruby-install-0.9.3.tar.gz \
|
||||
&& cd ruby-install-0.9.3/ \
|
||||
&& make install
|
||||
RUN ruby-install -p https://github.com/ruby/ruby/pull/9371.diff ruby 3.3.0
|
||||
ENV PATH="/opt/rubies/ruby-3.3.0/bin:${PATH}"
|
||||
|
||||
RUN apt-get install -y --no-install-recommends curl ldap-utils tini libvips
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
|
||||
20
Gemfile
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
|
||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||
|
||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||
gem 'rails', '~> 7.0.2'
|
||||
gem 'rails', '~> 7.1'
|
||||
# Use Puma as the app server
|
||||
gem 'puma', '~> 4.1'
|
||||
# View components
|
||||
@@ -22,7 +22,7 @@ gem 'jbuilder', '~> 2.7'
|
||||
# Use Redis adapter to run Action Cable in production
|
||||
# gem 'redis', '~> 4.0'
|
||||
# Use Active Model has_secure_password
|
||||
# gem 'bcrypt', '~> 3.1.7'
|
||||
gem 'bcrypt', '~> 3.1'
|
||||
|
||||
# Configuration
|
||||
gem 'dotenv-rails'
|
||||
@@ -37,6 +37,7 @@ gem 'devise_ldap_authenticatable'
|
||||
gem 'net-ldap'
|
||||
|
||||
# Utilities
|
||||
gem "image_processing", "~> 1.12.2"
|
||||
gem "rqrcode", "~> 2.0"
|
||||
gem 'rails-settings-cached', '~> 2.8.3'
|
||||
gem 'pagy', '~> 6.0', '>= 6.0.2'
|
||||
@@ -46,6 +47,8 @@ gem 'flipper-ui'
|
||||
|
||||
# HTTP requests
|
||||
gem 'faraday'
|
||||
gem 'down'
|
||||
gem 'aws-sdk-s3', require: false
|
||||
|
||||
# Background/scheduled jobs
|
||||
gem 'sidekiq', '< 7'
|
||||
@@ -58,18 +61,19 @@ 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'
|
||||
gem 'manifique'
|
||||
gem 'nostr'
|
||||
|
||||
group :development, :test do
|
||||
# Use sqlite3 as the database for Active Record
|
||||
gem 'sqlite3', '~> 1.4'
|
||||
gem 'sqlite3', '~> 1.7.2'
|
||||
gem 'rspec-rails'
|
||||
gem "byebug", "~> 11.1"
|
||||
gem 'rails-controller-testing'
|
||||
end
|
||||
|
||||
group :development do
|
||||
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
|
||||
gem 'web-console', '>= 3.3.0'
|
||||
gem 'web-console', '~> 4.2'
|
||||
gem 'listen', '~> 3.2'
|
||||
gem 'letter_opener'
|
||||
gem 'letter_opener_web'
|
||||
@@ -85,8 +89,8 @@ group :test do
|
||||
end
|
||||
|
||||
group :production do
|
||||
# Use postgresql as the database for Active Record
|
||||
gem 'pg', '~> 1.2.3'
|
||||
gem 'pg', '~> 1.5'
|
||||
end
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
|
||||
|
||||
427
Gemfile.lock
@@ -1,113 +1,127 @@
|
||||
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
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actioncable (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activestorage (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailer (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
actionview (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.3)
|
||||
actionview (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activestorage (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionview (7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activerecord (7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activestorage (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activemodel (7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
activerecord (7.1.3)
|
||||
activemodel (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.5)
|
||||
activesupport (7.1.3)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.4)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.886.0)
|
||||
aws-sdk-core (3.191.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.77.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.143.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
backport (1.2.0)
|
||||
bcrypt (3.1.18)
|
||||
bech32 (1.3.0)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bech32 (1.4.2)
|
||||
thor (>= 1.1.0)
|
||||
benchmark (0.2.1)
|
||||
benchmark (0.3.0)
|
||||
bigdecimal (3.1.6)
|
||||
bindex (0.8.1)
|
||||
bip-schnorr (0.6.0)
|
||||
bip-schnorr (0.7.0)
|
||||
ecdsa_ext (~> 0.5.0)
|
||||
builder (3.2.4)
|
||||
byebug (11.1.3)
|
||||
capybara (3.39.2)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.4.1)
|
||||
crack (0.4.5)
|
||||
crack (0.4.6)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cssbundling-rails (1.1.2)
|
||||
cssbundling-rails (1.4.0)
|
||||
railties (>= 6.0.0)
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@@ -115,8 +129,8 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.3)
|
||||
devise (4.9.2)
|
||||
date (3.3.4)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -125,7 +139,7 @@ GEM
|
||||
devise_ldap_authenticatable (0.8.7)
|
||||
devise (>= 3.4.1)
|
||||
net-ldap (>= 0.16.0)
|
||||
diff-lcs (1.5.0)
|
||||
diff-lcs (1.5.1)
|
||||
discourse_api (2.0.1)
|
||||
faraday (~> 2.7)
|
||||
faraday-follow_redirects
|
||||
@@ -135,6 +149,10 @@ GEM
|
||||
dotenv-rails (2.8.1)
|
||||
dotenv (= 2.8.1)
|
||||
railties (>= 3.2)
|
||||
down (5.4.1)
|
||||
addressable (~> 2.8)
|
||||
drb (2.2.0)
|
||||
ruby2_keywords
|
||||
e2mmap (0.1.0)
|
||||
ecdsa (1.2.0)
|
||||
ecdsa_ext (0.5.0)
|
||||
@@ -144,56 +162,66 @@ GEM
|
||||
tzinfo
|
||||
event_emitter (0.2.6)
|
||||
eventmachine (1.2.7)
|
||||
factory_bot (6.2.1)
|
||||
factory_bot (6.4.6)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
faker (3.2.3)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.7.6)
|
||||
faraday-net_http (>= 2.0, < 3.1)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday (2.9.0)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.0.2)
|
||||
faye-websocket (0.11.2)
|
||||
faraday-net_http (3.1.0)
|
||||
net-http
|
||||
faye-websocket (0.11.3)
|
||||
eventmachine (>= 0.12.0)
|
||||
websocket-driver (>= 0.5.1)
|
||||
ffi (1.15.5)
|
||||
flipper (0.28.0)
|
||||
ffi (1.16.3)
|
||||
flipper (1.2.2)
|
||||
concurrent-ruby (< 2)
|
||||
flipper-active_record (0.28.0)
|
||||
flipper-active_record (1.2.2)
|
||||
activerecord (>= 4.2, < 8)
|
||||
flipper (~> 0.28.0)
|
||||
flipper-ui (0.28.0)
|
||||
flipper (~> 1.2.2)
|
||||
flipper-ui (1.2.2)
|
||||
erubi (>= 1.0.0, < 2.0.0)
|
||||
flipper (~> 0.28.0)
|
||||
rack (>= 1.4, < 3)
|
||||
flipper (~> 1.2.2)
|
||||
rack (>= 1.4, < 4)
|
||||
rack-protection (>= 1.5.3, <= 4.0.0)
|
||||
sanitize (< 7)
|
||||
fugit (1.8.1)
|
||||
fugit (1.9.0)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
hashdiff (1.0.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
hashdiff (1.1.0)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
importmap-rails (1.1.6)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
importmap-rails (2.0.1)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.11.1)
|
||||
rdoc
|
||||
reline (>= 0.4.2)
|
||||
jaro_winkler (1.5.6)
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
json (2.6.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.1)
|
||||
kramdown (2.4.0)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
@@ -206,10 +234,10 @@ GEM
|
||||
listen (3.8.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
lnurl (1.0.1)
|
||||
lnurl (1.1.0)
|
||||
bech32 (~> 1.1)
|
||||
lockbox (1.2.0)
|
||||
loofah (2.21.3)
|
||||
lockbox (1.3.2)
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -217,60 +245,92 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
manifique (1.0.1)
|
||||
faraday (~> 2.9.0)
|
||||
faraday-follow_redirects (= 0.3.0)
|
||||
nokogiri (~> 1.16.0)
|
||||
marcel (1.0.2)
|
||||
matrix (0.4.2)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.18.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.21.2)
|
||||
multipart-post (2.3.0)
|
||||
net-imap (0.3.6)
|
||||
mutex_m (0.2.0)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.9.1)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.18.0)
|
||||
net-ldap (0.19.0)
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.3.3)
|
||||
net-smtp (0.4.0.1)
|
||||
net-protocol
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.2-arm64-darwin)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.0)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-x86_64-linux)
|
||||
nokogiri (1.16.0-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
nostr (0.5.0)
|
||||
bech32 (~> 1.4)
|
||||
bip-schnorr (~> 0.6)
|
||||
ecdsa (~> 1.2)
|
||||
event_emitter (~> 0.2)
|
||||
faye-websocket (~> 0.11)
|
||||
json (~> 2.6)
|
||||
orm_adapter (0.5.0)
|
||||
pagy (6.0.4)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.3)
|
||||
pagy (6.4.3)
|
||||
parallel (1.24.0)
|
||||
parser (3.3.0.5)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.2.3)
|
||||
public_suffix (5.0.1)
|
||||
pg (1.5.4)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.0.4)
|
||||
puma (4.3.12)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.7)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.5)
|
||||
actioncable (= 7.0.5)
|
||||
actionmailbox (= 7.0.5)
|
||||
actionmailer (= 7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actiontext (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.3)
|
||||
actioncable (= 7.1.3)
|
||||
actionmailbox (= 7.1.3)
|
||||
actionmailer (= 7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
actiontext (= 7.1.3)
|
||||
actionview (= 7.1.3)
|
||||
activejob (= 7.1.3)
|
||||
activemodel (= 7.1.3)
|
||||
activerecord (= 7.1.3)
|
||||
activestorage (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.5)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
railties (= 7.1.3)
|
||||
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.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
@@ -278,27 +338,32 @@ GEM
|
||||
rails-settings-cached (2.8.3)
|
||||
activerecord (>= 5.0.0)
|
||||
railties (>= 5.0.0)
|
||||
railties (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
method_source
|
||||
railties (7.1.3)
|
||||
actionpack (= 7.1.3)
|
||||
activesupport (= 7.1.3)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rbs (2.8.4)
|
||||
rdoc (6.6.2)
|
||||
psych (>= 4.0.0)
|
||||
redis (4.8.1)
|
||||
regexp_parser (2.8.1)
|
||||
responders (3.1.0)
|
||||
regexp_parser (2.9.0)
|
||||
reline (0.4.2)
|
||||
io-console (~> 0.5)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.2.5)
|
||||
rexml (3.2.6)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
@@ -308,10 +373,10 @@ GEM
|
||||
rspec-expectations (3.12.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.5)
|
||||
rspec-mocks (3.12.6)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.0.3)
|
||||
rspec-rails (6.1.1)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
@@ -319,32 +384,35 @@ GEM
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-support (3.12.0)
|
||||
rubocop (1.52.1)
|
||||
rspec-support (3.12.1)
|
||||
rubocop (1.60.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.2.3)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.0, < 2.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.0)
|
||||
rubocop-ast (1.30.0)
|
||||
parser (>= 3.2.1.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.0)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
rufus-scheduler (3.9.1)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
sanitize (6.0.1)
|
||||
sanitize (6.1.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
sentry-rails (5.9.0)
|
||||
sentry-rails (5.16.1)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.9.0)
|
||||
sentry-ruby (5.9.0)
|
||||
sentry-ruby (~> 5.16.1)
|
||||
sentry-ruby (5.16.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (6.5.9)
|
||||
sidekiq (6.5.12)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.5.0, < 5)
|
||||
@@ -352,7 +420,7 @@ GEM
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0)
|
||||
solargraph (0.49.0)
|
||||
solargraph (0.50.0)
|
||||
backport (~> 1.2)
|
||||
benchmark
|
||||
bundler (~> 2.0)
|
||||
@@ -368,56 +436,63 @@ GEM
|
||||
thor (~> 1.0)
|
||||
tilt (~> 2.0)
|
||||
yard (~> 0.9, >= 0.9.24)
|
||||
sprockets (4.2.0)
|
||||
sprockets (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.6.3-arm64-darwin)
|
||||
sqlite3 (1.6.3-x86_64-linux)
|
||||
stimulus-rails (1.2.1)
|
||||
sqlite3 (1.7.2)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (1.7.2-arm64-darwin)
|
||||
sqlite3 (1.7.2-x86_64-linux)
|
||||
stimulus-rails (1.3.3)
|
||||
railties (>= 6.0.0)
|
||||
thor (1.2.2)
|
||||
tilt (2.2.0)
|
||||
timeout (0.3.2)
|
||||
turbo-rails (1.4.0)
|
||||
stringio (3.1.0)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.1)
|
||||
turbo-rails (1.5.0)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.4.2)
|
||||
view_component (3.2.0)
|
||||
unicode-display_width (2.5.0)
|
||||
uri (0.13.0)
|
||||
view_component (3.10.0)
|
||||
activesupport (>= 5.2.0, < 8.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
method_source (~> 1.0)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.0)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.18.1)
|
||||
webmock (3.19.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
websocket-driver (0.7.5)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yard (0.9.34)
|
||||
zeitwerk (2.6.8)
|
||||
zeitwerk (2.6.12)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
byebug (~> 11.1)
|
||||
aws-sdk-s3
|
||||
bcrypt (~> 3.1)
|
||||
capybara
|
||||
cssbundling-rails
|
||||
database_cleaner
|
||||
@@ -425,12 +500,14 @@ DEPENDENCIES
|
||||
devise_ldap_authenticatable
|
||||
discourse_api
|
||||
dotenv-rails
|
||||
down
|
||||
factory_bot_rails
|
||||
faker
|
||||
faraday
|
||||
flipper
|
||||
flipper-active_record
|
||||
flipper-ui
|
||||
image_processing (~> 1.12.2)
|
||||
importmap-rails
|
||||
jbuilder (~> 2.7)
|
||||
letter_opener
|
||||
@@ -438,12 +515,14 @@ DEPENDENCIES
|
||||
listen (~> 3.2)
|
||||
lnurl
|
||||
lockbox
|
||||
manifique
|
||||
net-ldap
|
||||
nostr!
|
||||
nostr
|
||||
pagy (~> 6.0, >= 6.0.2)
|
||||
pg (~> 1.2.3)
|
||||
pg (~> 1.5)
|
||||
puma (~> 4.1)
|
||||
rails (~> 7.0.2)
|
||||
rails (~> 7.1)
|
||||
rails-controller-testing
|
||||
rails-settings-cached (~> 2.8.3)
|
||||
rqrcode (~> 2.0)
|
||||
rspec-rails
|
||||
@@ -453,14 +532,14 @@ DEPENDENCIES
|
||||
sidekiq-scheduler
|
||||
solargraph
|
||||
sprockets-rails
|
||||
sqlite3 (~> 1.4)
|
||||
sqlite3 (~> 1.7.2)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
view_component
|
||||
warden
|
||||
web-console (>= 3.3.0)
|
||||
web-console (~> 4.2)
|
||||
webmock
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.7
|
||||
2.5.5
|
||||
|
||||
57
README.md
@@ -14,8 +14,10 @@ so:
|
||||
|
||||
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||
Docker Desktop)
|
||||
3. Run `docker compose up` and wait until 389ds announces its successful start
|
||||
in the log output
|
||||
3. Run `docker compose up --build` and wait until all services have started
|
||||
(389ds might take an extra minute to be ready). This will take a while when
|
||||
running for the first time, so you might want to do something else in the
|
||||
meantime.
|
||||
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
||||
5. `docker compose run web rails ldap:setup`
|
||||
6. `docker compose run web rails db:setup`
|
||||
@@ -28,38 +30,44 @@ have the password "user is user".
|
||||
|
||||
### Rails app
|
||||
|
||||
_Note: when using Docker Compose, prefix the following commands with `docker-compose
|
||||
run web`._
|
||||
|
||||
Installing dependencies:
|
||||
|
||||
bundle install
|
||||
yarn install
|
||||
|
||||
Setting up local database (SQLite):
|
||||
Migrating the local database (after schema changes):
|
||||
|
||||
bundle exec rails db:create
|
||||
bundle exec rails db:migrate
|
||||
|
||||
Running the dev server and auto-building CSS files on change:
|
||||
Running the dev server, and auto-building CSS files on change _(automatic with Docker Compose)_:
|
||||
|
||||
bin/dev
|
||||
|
||||
Running the background workers (requires Redis):
|
||||
Running the background workers (requires Redis) _(automatic with Docker Compose)_:
|
||||
|
||||
bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
Running all specs:
|
||||
Running the test suite:
|
||||
|
||||
bundle exec rspec
|
||||
|
||||
### Docker (Compose)
|
||||
Running the test suite with Docker Compose requires overriding the Rails
|
||||
environment:
|
||||
|
||||
There is a working Docker Compose config file, which define a number of services including
|
||||
an app server for Rails as well as a local 389ds (LDAP) server.
|
||||
docker-compose run -e "RAILS_ENV=test" web rspec
|
||||
|
||||
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
|
||||
listening on port 389 on your machine.
|
||||
### Docker Compose
|
||||
|
||||
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`
|
||||
Services/containers are configured in `docker-compose.yml`.
|
||||
|
||||
You can run services selectively, for example if you want to run the Rails app
|
||||
and test suite on the host machine. Just add the service names of the
|
||||
containers you want to run to the `up` command, like so:
|
||||
|
||||
docker-compose up ldap redis
|
||||
|
||||
#### LDAP server
|
||||
|
||||
@@ -76,8 +84,24 @@ Now you can seed the back-end with data using this Rails task:
|
||||
The setup task will first delete any existing entries in the directory tree
|
||||
("dc=kosmos,dc=org"), and then create our development entries.
|
||||
|
||||
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.
|
||||
Note that all 389ds data is stored in the `389ds-data` volume. So if you want
|
||||
to start over with a fresh installation, delete both that volume as well as the
|
||||
container.
|
||||
|
||||
#### Minio / remoteStorage
|
||||
|
||||
If you want to run remoteStorage accounts locally, you will have to create the
|
||||
respective bucket first. With the `minio` container running (run by default
|
||||
when using Docker Compose), follow these steps:
|
||||
|
||||
* `docker compose up web redis minio liquor-cabinet`
|
||||
* Head to http://localhost:9001 and log in with user `minioadmin`, password
|
||||
`minioadmin`
|
||||
* Create a new bucket called `remotestorage` (or whatever you
|
||||
change the `S3_BUCKET` config to)
|
||||
* Create a new key with ID "dev-key" and secret "123456789" (or whatever you
|
||||
change `S3_ACCESS_KEY` and `S3_SECRET_KEY` to). Leave the policy field empty,
|
||||
as it will automatically allow access to the bucket you created.
|
||||
|
||||
### Adding npm modules to use with Stimulus controllers
|
||||
|
||||
@@ -107,6 +131,7 @@ command:
|
||||
* [Tailwind CSS](https://tailwindcss.com/)
|
||||
* [Sass](https://sass-lang.com/documentation)
|
||||
* [Stimulus](https://stimulus.hotwired.dev/handbook/)
|
||||
* [Tailwind Stimulus Components](https://github.com/excid3/tailwindcss-stimulus-components)
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<% if @image_url %>
|
||||
<%= image_tag @image_url, class: "h-full w-full" %>
|
||||
<% else %>
|
||||
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
|
||||
<% end %>
|
||||
21
app/components/app_catalog/web_app_icon_component.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AppCatalog
|
||||
class WebAppIconComponent < ViewComponent::Base
|
||||
def initialize(web_app:)
|
||||
if web_app&.icon&.attached?
|
||||
@image_url = image_url_for(web_app.icon)
|
||||
elsif web_app&.apple_touch_icon&.attached?
|
||||
@image_url = image_url_for(web_app.apple_touch_icon)
|
||||
end
|
||||
end
|
||||
|
||||
def image_url_for(attachment)
|
||||
if Setting.s3_enabled?
|
||||
s3_image_url(attachment)
|
||||
else
|
||||
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/components/app_info_component.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
19
app/components/app_info_component.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
34
app/components/dropdown_component.html.erb
Normal file
@@ -0,0 +1,34 @@
|
||||
<div data-controller="dropdown" data-action="click->dropdown#toggle click@window->dropdown#hide">
|
||||
<div class="relative inline-block">
|
||||
<div role="button" tabindex="0" data-dropdown-target="button"
|
||||
class="inline-block select-none">
|
||||
<% if @size == :large %>
|
||||
<span class="appearance-none flex items-center inline-block">
|
||||
<span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full">
|
||||
<%= render partial: "icons/#{@icon_name}",
|
||||
locals: { custom_class: "inline text-gray-500 h-6 w-6" } %>
|
||||
</span>
|
||||
</span>
|
||||
<% elsif @size == :small %>
|
||||
<span class="appearance-none flex items-center inline-block">
|
||||
<span class="text-gray-500 hover:text-blue-600">
|
||||
<%= render partial: "icons/#{@icon_name}",
|
||||
locals: { custom_class: "inline h-4 w-4" } %>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div data-dropdown-target="menu"
|
||||
data-transition-enter="transition ease-out duration-200"
|
||||
data-transition-enter-from="opacity-0 translate-y-1"
|
||||
data-transition-enter-to="opacity-100 translate-y-0"
|
||||
data-transition-leave="transition ease-in duration-150"
|
||||
data-transition-leave-from="opacity-100 translate-y-0"
|
||||
data-transition-leave-to="opacity-0 translate-y-1"
|
||||
class="hidden absolute top-4 right-0 z-10 mt-5 flex w-screen max-w-max">
|
||||
<div class="bg-white shadow-lg rounded border overflow-hidden w-auto">
|
||||
<%= content %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
8
app/components/dropdown_component.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DropdownComponent < ViewComponent::Base
|
||||
def initialize(size: :large, icon_name: "kebap-menu")
|
||||
@size = size.to_sym
|
||||
@icon_name = icon_name
|
||||
end
|
||||
end
|
||||
6
app/components/dropdown_link_component.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<%= link_to @href, class: @class, data: {
|
||||
'dropdown-target': "menuItem",
|
||||
'action': "keydown.up->dropdown#previousItem:prevent keydown.down->dropdown#nextItem:prevent"
|
||||
} do %>
|
||||
<%= content %>
|
||||
<% end %>
|
||||
18
app/components/dropdown_link_component.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DropdownLinkComponent < ViewComponent::Base
|
||||
def initialize(href:, separator: false, add_class: nil)
|
||||
@href = href
|
||||
@class = class_str(separator, add_class)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def class_str(separator, add_class)
|
||||
str = "no-underline block px-5 py-3 text-sm text-gray-900 bg-white
|
||||
hover:bg-gray-100 focus:bg-gray-100 whitespace-no-wrap"
|
||||
str = "#{str} border-t" if separator
|
||||
str = "#{str} #{add_class}" if add_class
|
||||
str
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ module FormElements
|
||||
@tag = tag
|
||||
@positioning = :vertical
|
||||
@title = title
|
||||
@descripton = description
|
||||
@description = description
|
||||
@key = key.to_sym
|
||||
@type = type
|
||||
@resettable = is_resettable?(@key)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
} : nil do %>
|
||||
<div class="flex flex-col">
|
||||
<label class="font-bold mb-1"><%= @title %></label>
|
||||
<% if @description.present? %>
|
||||
<p class="text-gray-500"><%= @descripton %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||
<%= render FormElements::ToggleComponent.new(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module FormElements
|
||||
class FieldsetToggleComponent < ViewComponent::Base
|
||||
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
|
||||
enabled: false, input_enabled: true, title:, description:)
|
||||
enabled: false, input_enabled: true, title:, description: nil)
|
||||
@tag = tag
|
||||
@form = form
|
||||
@attribute = attribute
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
<div data-modal-target="container"
|
||||
data-action="click->modal#closeBackground keyup@window->modal#closeWithKeyboard"
|
||||
class="hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center"
|
||||
style="z-index: 9999;">
|
||||
<div class="max-h-screen w-auto max-w-lg relative">
|
||||
<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">Close</button>
|
||||
<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 %>
|
||||
<% if @show_close_button %>
|
||||
<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>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
class ModalComponent < ViewComponent::Base
|
||||
def initialize(show_close_button: true)
|
||||
@show_close_button = show_close_button
|
||||
end
|
||||
end
|
||||
|
||||
26
app/components/rs_auth_component.html.erb
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="h-16 w-16 flex-none">
|
||||
<%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h4 class="mb-1 text-lg font-bold">
|
||||
<%= @web_app&.name || @auth.app_name %>
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= @auth.client_id %>
|
||||
</p>
|
||||
</div>
|
||||
<%= render DropdownComponent.new do %>
|
||||
<%= render DropdownLinkComponent.new(
|
||||
href: launch_app_services_storage_rs_auth_url(@auth)
|
||||
) do %>
|
||||
Launch app
|
||||
<% end %>
|
||||
<%= render DropdownLinkComponent.new(
|
||||
href: revoke_services_storage_rs_auth_url(@auth),
|
||||
separator: true, add_class: "text-red-700"
|
||||
) do %>
|
||||
Revoke access
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
8
app/components/rs_auth_component.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RsAuthComponent < ViewComponent::Base
|
||||
def initialize(auth:)
|
||||
@auth = auth
|
||||
@web_app = auth.web_app
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,8 @@
|
||||
<%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %>
|
||||
<% if @icon.present? %>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SidenavLinkComponent < ViewComponent::Base
|
||||
def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
|
||||
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil,
|
||||
active: false, disabled: false)
|
||||
@name = name
|
||||
@level = level
|
||||
@path = path
|
||||
@icon = icon
|
||||
@text_icon = text_icon
|
||||
@active = active
|
||||
@disabled = disabled
|
||||
@link_class = class_names_link(path)
|
||||
|
||||
9
app/controllers/admin/app_catalog/web_apps_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class Admin::AppCatalog::WebAppsController < Admin::AppCatalogController
|
||||
def index
|
||||
@pagy, @web_apps = pagy(AppCatalog::WebApp.order('created_at desc'))
|
||||
|
||||
@stats = {
|
||||
known_apps: AppCatalog::WebApp.count
|
||||
}
|
||||
end
|
||||
end
|
||||
9
app/controllers/admin/app_catalog_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class Admin::AppCatalogController < Admin::BaseController
|
||||
before_action :set_current_section
|
||||
|
||||
private
|
||||
|
||||
def set_current_section
|
||||
@current_section = :app_catalog
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,8 @@
|
||||
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||
def index
|
||||
def show
|
||||
end
|
||||
|
||||
def create
|
||||
def update
|
||||
update_settings
|
||||
|
||||
redirect_to admin_settings_registrations_path, flash: {
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
class Admin::Settings::ServicesController < Admin::SettingsController
|
||||
def index
|
||||
@service = params[:s]
|
||||
before_action :set_service, only: [:show, :update]
|
||||
|
||||
if @service.blank?
|
||||
redirect_to admin_settings_services_path(params: { s: "discourse" })
|
||||
end
|
||||
def index
|
||||
redirect_to admin_settings_service_path("btcpay")
|
||||
end
|
||||
|
||||
def create
|
||||
service = params.require(:service)
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
update_settings
|
||||
|
||||
redirect_to admin_settings_services_path(params: { s: service }), flash: {
|
||||
redirect_to admin_settings_service_path(@service), flash: {
|
||||
success: "Settings saved"
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_subsection
|
||||
@subsection = "services"
|
||||
end
|
||||
|
||||
def set_service
|
||||
@service = params[:service]
|
||||
|
||||
if @service.blank?
|
||||
redirect_to admin_settings_services_path and return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ class Admin::SettingsController < Admin::BaseController
|
||||
end
|
||||
|
||||
if @errors.any?
|
||||
render :index and return
|
||||
render :show and return
|
||||
end
|
||||
|
||||
changed_keys.each do |key|
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
class Admin::UsersController < Admin::BaseController
|
||||
before_action :set_user, only: [:show]
|
||||
before_action :set_user, except: [:index]
|
||||
before_action :set_current_section
|
||||
|
||||
# GET /admin/users
|
||||
def index
|
||||
ldap = LdapService.new
|
||||
@ou = params[:ou] || Setting.primary_domain
|
||||
@orgs = ldap.fetch_organizations
|
||||
@ou = Setting.primary_domain
|
||||
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
|
||||
|
||||
@stats = {
|
||||
@@ -14,19 +14,46 @@ class Admin::UsersController < Admin::BaseController
|
||||
}
|
||||
end
|
||||
|
||||
# GET /admin/users/:username
|
||||
def show
|
||||
if Setting.lndhub_admin_enabled?
|
||||
@lndhub_user = @user.lndhub_user
|
||||
end
|
||||
|
||||
@services_enabled = @user.services_enabled
|
||||
|
||||
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
|
||||
end
|
||||
|
||||
# POST /admin/users/:username/invitations
|
||||
def create_invitations
|
||||
amount = params[:amount].to_i
|
||||
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
|
||||
|
||||
CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
|
||||
|
||||
redirect_to admin_user_path(@user.cn), flash: {
|
||||
success: "Added #{amount} invitations to #{@user.cn}'s account"
|
||||
}
|
||||
end
|
||||
|
||||
# DELETE /admin/users/:username/invitations
|
||||
def delete_invitations
|
||||
invitations = @user.invitations.unused
|
||||
amount = invitations.count
|
||||
|
||||
invitations.destroy_all
|
||||
|
||||
redirect_to admin_user_path(@user.cn), flash: {
|
||||
success: "Removed #{amount} invitations from #{@user.cn}'s account"
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
address = params[:address].split("@")
|
||||
@user = User.where(cn: address.first, ou: address.last).first
|
||||
@user = User.find_by(cn: params[:username], ou: Setting.primary_domain)
|
||||
http_status :not_found unless @user
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
|
||||
37
app/controllers/api/btcpay_controller.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class Api::BtcpayController < Api::BaseController
|
||||
before_action :require_feature_enabled
|
||||
before_action :set_cors_access_control_headers
|
||||
|
||||
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
|
||||
|
||||
def set_cors_access_control_headers
|
||||
return unless Rails.env.development?
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Headers'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
@@ -37,4 +37,8 @@ class ApplicationController < ActionController::Base
|
||||
format.any { head status }
|
||||
end
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(user)
|
||||
session[:user_return_to] || root_path
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class LnurlpayController < ApplicationController
|
||||
before_action :check_feature_enabled
|
||||
before_action :find_user_by_address
|
||||
before_action :check_service_available
|
||||
before_action :find_user
|
||||
|
||||
MIN_SATS = 10
|
||||
MAX_SATS = 1_000_000
|
||||
@@ -9,7 +9,7 @@ class LnurlpayController < ApplicationController
|
||||
def index
|
||||
render json: {
|
||||
status: "OK",
|
||||
callback: "https://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
|
||||
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
|
||||
tag: "payRequest",
|
||||
maxSendable: MAX_SATS * 1000, # msat
|
||||
minSendable: MIN_SATS * 1000, # msat
|
||||
@@ -34,8 +34,8 @@ class LnurlpayController < ApplicationController
|
||||
|
||||
def invoice
|
||||
amount = params[:amount].to_i / 1000 # msats
|
||||
address = params[:address]
|
||||
comment = params[:comment] || ""
|
||||
address = @user.address
|
||||
|
||||
if !valid_amount?(amount)
|
||||
render json: { status: "ERROR", reason: "Invalid amount" }
|
||||
@@ -69,9 +69,8 @@ class LnurlpayController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def find_user_by_address
|
||||
address = params[:address].split("@")
|
||||
@user = User.where(cn: address.first, ou: address.last).first
|
||||
def find_user
|
||||
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
|
||||
http_status :not_found if @user.nil?
|
||||
end
|
||||
|
||||
@@ -89,7 +88,7 @@ class LnurlpayController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def check_feature_enabled
|
||||
def check_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
class Rs::OauthController < ApplicationController
|
||||
before_action :require_user_signed_in
|
||||
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
|
||||
@user = User.where(cn: params[:username].downcase, ou: Setting.primary_domain).first
|
||||
@scopes = parse_scopes params[:scope]
|
||||
@redirect_uri = params[:redirect_uri]
|
||||
@client_id = params[:client_id]
|
||||
@@ -21,7 +21,7 @@ class Rs::OauthController < ApplicationController
|
||||
unless current_user == @user
|
||||
sign_out :user
|
||||
|
||||
redirect_to new_rs_oauth_url(@user.address,
|
||||
redirect_to new_rs_oauth_url(@user.cn,
|
||||
scope: params[:scope],
|
||||
redirect_uri: params[:redirect_uri],
|
||||
client_id: params[:client_id],
|
||||
@@ -30,23 +30,26 @@ class Rs::OauthController < ApplicationController
|
||||
end
|
||||
|
||||
unless @client_id.present?
|
||||
redirect_to url_with_state("#{@redirect_uri}#error=invalid_request", @state) and return
|
||||
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) and return
|
||||
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) and return
|
||||
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_client", @state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
@client_id.gsub!(/http(s)?:\/\//, "")
|
||||
|
||||
# TODO
|
||||
# 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
|
||||
# end
|
||||
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
|
||||
@@ -64,15 +67,18 @@ class Rs::OauthController < ApplicationController
|
||||
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
|
||||
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
|
||||
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
|
||||
redirect_to(url_with_state("#{redirect_uri}#error=invalid_client", state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
client_id.gsub!(/http(s)?:\/\//, "")
|
||||
@@ -81,26 +87,21 @@ class Rs::OauthController < ApplicationController
|
||||
permissions: permissions,
|
||||
client_id: client_id,
|
||||
redirect_uri: redirect_uri,
|
||||
app_name: client_id, #TODO use user-defined name
|
||||
app_name: client_id,
|
||||
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)
|
||||
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state),
|
||||
allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def app_auth_url(auth)
|
||||
url = "#{auth.url}#remotestorage=#{current_user.address}"
|
||||
url += "&access_token=#{auth.token}"
|
||||
url
|
||||
def require_signed_in_with_username
|
||||
unless user_signed_in?
|
||||
session[:user_return_to] = request.url
|
||||
redirect_to new_user_session_path(cn: params[:username], ou: Setting.primary_domain)
|
||||
end
|
||||
end
|
||||
|
||||
def hostname_of(uri)
|
||||
|
||||
9
app/controllers/services/base_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
class Services::BaseController < ApplicationController
|
||||
before_action :set_current_section
|
||||
|
||||
private
|
||||
|
||||
def set_current_section
|
||||
@current_section = :services
|
||||
end
|
||||
end
|
||||
14
app/controllers/services/chat_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
34
app/controllers/services/email_controller.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
class Services::EmailController < Services::BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :require_service_available
|
||||
before_action :require_feature_enabled
|
||||
|
||||
def show
|
||||
ldap_entry = current_user.ldap_entry
|
||||
|
||||
@service_enabled = ldap_entry[:email_password].present?
|
||||
@maildrop = ldap_entry[:email_maildrop]
|
||||
@email_forwarding_active = @maildrop.present? &&
|
||||
@maildrop.split("@").first != current_user.cn
|
||||
end
|
||||
|
||||
def new_password
|
||||
if session[:new_email_password].present?
|
||||
@new_password = session.delete(:new_email_password)
|
||||
else
|
||||
redirect_to setting_path(:email)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.email_enabled?
|
||||
end
|
||||
|
||||
def require_feature_enabled
|
||||
unless Flipper.enabled?(:email, current_user)
|
||||
http_status :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/controllers/services/mastodon_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -1,30 +1,26 @@
|
||||
class Services::RemotestorageController < ApplicationController
|
||||
before_action :require_user_signed_in
|
||||
before_action :require_service_enabled
|
||||
class Services::RemotestorageController < Services::BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :require_service_available
|
||||
before_action :require_feature_enabled
|
||||
before_action :set_current_section
|
||||
|
||||
def dashboard
|
||||
# Dashboard
|
||||
def show
|
||||
# unless current_user.services_enabled.include?(:remotestorage)
|
||||
# redirect_to service_remotestorage_info_path
|
||||
# end
|
||||
@rs_auths = current_user.remote_storage_authorizations
|
||||
# TODO sort by app name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.remotestorage_enabled?
|
||||
end
|
||||
|
||||
def require_feature_enabled
|
||||
unless Flipper.enabled?(:remotestorage, current_user)
|
||||
http_status :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
def require_service_enabled
|
||||
unless Setting.remotestorage_enabled?
|
||||
http_status :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
@current_section = :services
|
||||
end
|
||||
end
|
||||
|
||||
42
app/controllers/services/rs_auths_controller.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
class Services::RsAuthsController < Services::BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :require_feature_enabled
|
||||
before_action :require_service_available
|
||||
# before_action :require_service_enabled
|
||||
before_action :find_rs_auth
|
||||
|
||||
def destroy
|
||||
@auth.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html do redirect_to services_storage_url, flash: {
|
||||
success: 'App authorization revoked'
|
||||
}
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def launch_app
|
||||
launch_url = "#{@auth.launch_url}#remotestorage=#{current_user.address}&access_token=#{@auth.token}"
|
||||
|
||||
redirect_to launch_url, allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_feature_enabled
|
||||
unless Flipper.enabled?(:remotestorage, current_user)
|
||||
http_status :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.remotestorage_enabled?
|
||||
end
|
||||
|
||||
def find_rs_auth
|
||||
@auth = current_user.remote_storage_authorizations.find(params[:id])
|
||||
http_status :not_found unless @auth.present?
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,11 @@
|
||||
require 'securerandom'
|
||||
require "securerandom"
|
||||
require "bcrypt"
|
||||
|
||||
class SettingsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_main_nav_section
|
||||
before_action :set_settings_section, only: [:show, :update, :update_email]
|
||||
before_action :set_user, only: [:show, :update, :update_email]
|
||||
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password]
|
||||
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
|
||||
|
||||
def index
|
||||
redirect_to setting_path(:profile)
|
||||
@@ -19,10 +20,15 @@ class SettingsController < ApplicationController
|
||||
def update
|
||||
@user.preferences.merge!(user_params[:preferences] || {})
|
||||
@user.display_name = user_params[:display_name]
|
||||
@user.avatar_new = user_params[:avatar]
|
||||
|
||||
if @user.save
|
||||
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
|
||||
LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
|
||||
LdapManager::UpdateDisplayName.call(dn: @user.dn, display_name: @user.display_name)
|
||||
end
|
||||
|
||||
if @user.avatar_new.present?
|
||||
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
|
||||
end
|
||||
|
||||
redirect_to setting_path(@settings_section), flash: {
|
||||
@@ -35,7 +41,7 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def update_email
|
||||
if @user.valid_ldap_authentication?(email_params[:current_password])
|
||||
if @user.valid_ldap_authentication?(security_params[:current_password])
|
||||
if @user.update email: email_params[:email]
|
||||
redirect_to setting_path(:account), flash: {
|
||||
notice: 'Please confirm your new address using the confirmation link we just sent you.'
|
||||
@@ -51,6 +57,28 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def reset_email_password
|
||||
@user.current_password = security_params[:current_password]
|
||||
|
||||
if @user.valid_ldap_authentication?(@user.current_password)
|
||||
@user.current_password = nil
|
||||
session[:new_email_password] = generate_email_password
|
||||
hashed_password = hash_email_password(session[:new_email_password])
|
||||
LdapManager::UpdateEmailPassword.call(dn: @user.dn, password_hash: hashed_password)
|
||||
|
||||
if @user.ldap_entry[:email_maildrop] != @user.address
|
||||
LdapManager::UpdateEmailMaildrop.call(dn: @user.dn, address: @user.address)
|
||||
end
|
||||
|
||||
redirect_to new_password_services_email_path
|
||||
else
|
||||
@validation_errors = {
|
||||
current_password: [ "Wrong password. Try again!" ]
|
||||
}
|
||||
render :show, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
def reset_password
|
||||
current_user.send_reset_password_instructions
|
||||
sign_out current_user
|
||||
@@ -60,8 +88,8 @@ class SettingsController < ApplicationController
|
||||
|
||||
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_valid_id = NostrManager::ValidateId.call(event: signed_event)
|
||||
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
|
||||
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
|
||||
|
||||
unless is_valid_id && is_valid_sig && is_correct_content
|
||||
@@ -105,7 +133,10 @@ class SettingsController < ApplicationController
|
||||
|
||||
def set_settings_section
|
||||
@settings_section = params[:section]
|
||||
allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments]
|
||||
allowed_sections = [
|
||||
:profile, :account, :xmpp, :email, :lightning, :remotestorage,
|
||||
:experiments
|
||||
]
|
||||
|
||||
unless allowed_sections.include?(@settings_section.to_sym)
|
||||
redirect_to setting_path(:profile)
|
||||
@@ -117,14 +148,19 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:display_name, preferences: [
|
||||
params.require(:user).permit(:display_name, :avatar, preferences: [
|
||||
:lightning_notify_sats_received,
|
||||
:remotestorage_notify_auth_created,
|
||||
:xmpp_exchange_contacts_with_invitees
|
||||
])
|
||||
end
|
||||
|
||||
def email_params
|
||||
params.require(:user).permit(:email, :current_password)
|
||||
params.require(:user).permit(:email)
|
||||
end
|
||||
|
||||
def security_params
|
||||
params.require(:user).permit(:current_password)
|
||||
end
|
||||
|
||||
def nostr_event_params
|
||||
@@ -132,4 +168,14 @@ class SettingsController < ApplicationController
|
||||
:id, :pubkey, :created_at, :kind, :tags, :content, :sig
|
||||
])
|
||||
end
|
||||
|
||||
def generate_email_password
|
||||
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
||||
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
|
||||
end
|
||||
|
||||
def hash_email_password(password)
|
||||
salt = BCrypt::Engine.generate_salt
|
||||
BCrypt::Engine.hash_secret(password, salt)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,13 +96,13 @@ class SignupController < ApplicationController
|
||||
session[:new_user] = nil
|
||||
session[:validation_error] = nil
|
||||
|
||||
CreateAccount.call(
|
||||
CreateAccount.call(account: {
|
||||
username: @user.cn,
|
||||
domain: Setting.primary_domain,
|
||||
email: @user.email,
|
||||
password: @user.password,
|
||||
invitation: @invitation
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
def set_context
|
||||
|
||||
@@ -6,15 +6,19 @@ class WebfingerController < ApplicationController
|
||||
def show
|
||||
resource = params[:resource]
|
||||
|
||||
if resource && resource.match(/acct:\w+/)
|
||||
useraddress = resource.split(":").last
|
||||
username, org = useraddress.split("@")
|
||||
username.downcase!
|
||||
unless User.where(cn: username, ou: org).any?
|
||||
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
|
||||
@username, @org = @useraddress.split("@")
|
||||
|
||||
unless Rails.env.development?
|
||||
# Allow different domains (e.g. localhost:3000) in development only
|
||||
head 404 and return unless @org == Setting.primary_domain
|
||||
end
|
||||
|
||||
unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any?
|
||||
head 404 and return
|
||||
end
|
||||
|
||||
render json: webfinger(useraddress).to_json,
|
||||
render json: webfinger.to_json,
|
||||
content_type: "application/jrd+json"
|
||||
else
|
||||
head 422 and return
|
||||
@@ -23,19 +27,18 @@ class WebfingerController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def webfinger(useraddress)
|
||||
def webfinger
|
||||
links = [];
|
||||
|
||||
links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
|
||||
# TODO check if storage service is enabled for user, not just globally
|
||||
links << remotestorage_link if Setting.remotestorage_enabled
|
||||
|
||||
{ "links" => links }
|
||||
end
|
||||
|
||||
def remotestorage_link(useraddress)
|
||||
# TODO use when OAuth routes are available
|
||||
# auth_url = new_rs_oauth_url(useraddress)
|
||||
auth_url = "https://example.com/rs/oauth"
|
||||
storage_url = "#{Setting.rs_storage_url}/#{useraddress}"
|
||||
def remotestorage_link
|
||||
auth_url = new_rs_oauth_url(@username)
|
||||
storage_url = "#{Setting.rs_storage_url}/#{@username}"
|
||||
|
||||
{
|
||||
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
|
||||
@@ -51,7 +54,8 @@ class WebfingerController < ApplicationController
|
||||
end
|
||||
|
||||
def allow_cross_origin_requests
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
|
||||
return unless Rails.env.development?
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ class WebhooksController < ApplicationController
|
||||
def notify_xmpp(address, amt_sats, memo)
|
||||
payload = {
|
||||
type: "normal",
|
||||
from: Setting.primary_domain,
|
||||
from: Setting.xmpp_notifications_from_address,
|
||||
to: address,
|
||||
subject: "Sats received!",
|
||||
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Application } from "@hotwired/stimulus"
|
||||
import { Modal } from "tailwindcss-stimulus-components"
|
||||
import { Dropdown, Modal, Tabs } from "tailwindcss-stimulus-components"
|
||||
|
||||
const application = Application.start()
|
||||
|
||||
application.register('dropdown', Dropdown)
|
||||
application.register('modal', Modal)
|
||||
application.register('tabs', Tabs)
|
||||
|
||||
// Configure Stimulus development experience
|
||||
application.debug = false
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "resetPasswordButton", "currentPasswordField" ]
|
||||
static values = { validationFailed: Boolean }
|
||||
|
||||
connect () {
|
||||
if (this.validationFailedValue) return;
|
||||
|
||||
this.element.querySelectorAll(".initial-hidden").forEach(el => {
|
||||
el.classList.add("hidden");
|
||||
})
|
||||
this.element.querySelectorAll(".initial-visible").forEach(el => {
|
||||
el.classList.remove("hidden");
|
||||
})
|
||||
}
|
||||
|
||||
showPasswordReset () {
|
||||
this.element.querySelectorAll(".initial-visible").forEach(el => {
|
||||
el.classList.add("hidden");
|
||||
})
|
||||
this.element.querySelectorAll(".initial-hidden").forEach(el => {
|
||||
el.classList.remove("hidden");
|
||||
})
|
||||
this.currentPasswordFieldTarget.select();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,4 @@
|
||||
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 {
|
||||
@@ -15,10 +6,6 @@ export default class extends Controller {
|
||||
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
|
||||
@@ -53,11 +40,6 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -5,4 +5,22 @@ class NotificationMailer < ApplicationMailer
|
||||
@subject = "Sats received"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
|
||||
def remotestorage_auth_created
|
||||
@user = params[:user]
|
||||
@auth = params[:auth]
|
||||
@permissions = @auth.permissions.map do |p|
|
||||
access = p.split(":")[1] == 'r' ? 'read' : 'read/write'
|
||||
directory = p.split(':')[0] == '' ? 'all folders and files' : p.split(':')[0]
|
||||
"#{access} #{directory}"
|
||||
end
|
||||
@subject = "New app connected to your storage"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
|
||||
def new_invitations_available
|
||||
@user = params[:user]
|
||||
@subject = "New invitations added to your account"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
end
|
||||
|
||||
5
app/models/app_catalog.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module AppCatalog
|
||||
def self.table_name_prefix
|
||||
"app_catalog_"
|
||||
end
|
||||
end
|
||||
16
app/models/app_catalog/web_app.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class AppCatalog::WebApp < ApplicationRecord
|
||||
store :metadata, coder: JSON
|
||||
|
||||
has_many :remote_storage_authorizations, dependent: :destroy
|
||||
|
||||
has_one_attached :icon
|
||||
has_one_attached :apple_touch_icon
|
||||
|
||||
validates :url, presence: true, uniqueness: true
|
||||
validates :url, format: { with: URI.regexp },
|
||||
if: Proc.new { |a| a.url.present? }
|
||||
|
||||
def update_metadata
|
||||
AppCatalogManager::UpdateMetadata.call(app: self)
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,8 @@
|
||||
class RemoteStorageAuthorization < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
|
||||
|
||||
serialize :permissions
|
||||
serialize :permissions unless Rails.env.production?
|
||||
|
||||
validates_presence_of :permissions
|
||||
validates_presence_of :client_id
|
||||
@@ -15,22 +16,36 @@ class RemoteStorageAuthorization < ApplicationRecord
|
||||
|
||||
before_create :generate_token
|
||||
before_create :store_token_in_redis
|
||||
before_create :find_or_create_web_app
|
||||
after_create :schedule_token_expiry
|
||||
after_create :notify_user
|
||||
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}"
|
||||
uri = URI.parse self.redirect_uri
|
||||
"#{uri.scheme}://#{client_id}"
|
||||
end
|
||||
|
||||
def launch_url
|
||||
return url unless web_app && web_app.metadata[:start_url].present?
|
||||
|
||||
start_url = web_app.metadata[:start_url]
|
||||
|
||||
if start_url.match("^https?:\/\/")
|
||||
return start_url.start_with?(url) ? start_url : url
|
||||
else
|
||||
"http://#{client_id}"
|
||||
path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "")
|
||||
"#{url}/#{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def delete_token_from_redis
|
||||
key = "rs:authorizations:#{user.address}:#{token}"
|
||||
key = "authorizations:#{user.cn}:#{token}"
|
||||
redis.srem? key, redis.smembers(key)
|
||||
rescue => e
|
||||
Rails.logger.error e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
private
|
||||
@@ -44,7 +59,7 @@ class RemoteStorageAuthorization < ApplicationRecord
|
||||
end
|
||||
|
||||
def store_token_in_redis
|
||||
redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions
|
||||
redis.sadd "authorizations:#{user.cn}:#{token}", permissions
|
||||
end
|
||||
|
||||
def schedule_token_expiry
|
||||
@@ -60,4 +75,40 @@ class RemoteStorageAuthorization < ApplicationRecord
|
||||
job.delete if job.display_args == [id]
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_web_app
|
||||
if looks_like_hosted_origin?
|
||||
web_app = AppCatalog::WebApp.find_or_create_by!(url: self.url)
|
||||
web_app.update_metadata unless web_app.name.present?
|
||||
self.web_app = web_app
|
||||
self.app_name = web_app.name.presence || client_id
|
||||
else
|
||||
self.app_name = client_id
|
||||
end
|
||||
end
|
||||
|
||||
def looks_like_hosted_origin?
|
||||
uri = URI.parse self.redirect_uri
|
||||
!!(uri.host =~ /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/)
|
||||
rescue URI::InvalidURIError
|
||||
false
|
||||
end
|
||||
|
||||
def notify_user
|
||||
notify = user.preferences[:remotestorage_notify_auth_created]
|
||||
|
||||
case notify
|
||||
when "xmpp"
|
||||
router = Router.new
|
||||
payload = {
|
||||
type: "normal", to: user.address,
|
||||
from: Setting.xmpp_notifications_from_address,
|
||||
body: "You have just granted '#{self.client_id}' access to your Kosmos Storage. Visit your Storage dashboard to check on your connected apps and revoke permissions anytime: #{router.services_storage_url}"
|
||||
}
|
||||
XmppSendMessageJob.perform_later(payload)
|
||||
when "email"
|
||||
NotificationMailer.with(user: user, auth: self)
|
||||
.remotestorage_auth_created.deliver_later
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,9 @@ class Setting < RailsSettings::Base
|
||||
field :redis_url, type: :string,
|
||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||
|
||||
field :s3_enabled, type: :boolean,
|
||||
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
|
||||
#
|
||||
# Registrations
|
||||
#
|
||||
@@ -36,7 +39,25 @@ class Setting < RailsSettings::Base
|
||||
#
|
||||
|
||||
field :sentry_enabled, type: :boolean, readonly: true,
|
||||
default: (ENV["SENTRY_DSN"].present?.to_s || false)
|
||||
default: ENV["SENTRY_DSN"].present?
|
||||
|
||||
#
|
||||
# BTCPay Server
|
||||
#
|
||||
|
||||
field :btcpay_api_url, type: :string,
|
||||
default: ENV["BTCPAY_API_URL"].presence
|
||||
|
||||
field :btcpay_enabled, type: :boolean,
|
||||
default: ENV["BTCPAY_API_URL"].present?
|
||||
|
||||
field :btcpay_store_id, type: :string,
|
||||
default: ENV["BTCPAY_STORE_ID"].presence
|
||||
|
||||
field :btcpay_auth_token, type: :string,
|
||||
default: ENV["BTCPAY_AUTH_TOKEN"].presence
|
||||
|
||||
field :btcpay_publish_wallet_balances, type: :boolean, default: true
|
||||
|
||||
#
|
||||
# Discourse
|
||||
@@ -46,17 +67,27 @@ class Setting < RailsSettings::Base
|
||||
default: ENV["DISCOURSE_PUBLIC_URL"].presence
|
||||
|
||||
field :discourse_enabled, type: :boolean,
|
||||
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
|
||||
default: ENV["DISCOURSE_PUBLIC_URL"].present?
|
||||
|
||||
field :discourse_connect_secret, type: :string,
|
||||
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
|
||||
|
||||
#
|
||||
# Drone CI
|
||||
#
|
||||
|
||||
field :droneci_public_url, type: :string,
|
||||
default: ENV["DRONECI_PUBLIC_URL"].presence
|
||||
|
||||
field :droneci_enabled, type: :boolean,
|
||||
default: ENV["DRONECI_PUBLIC_URL"].present?
|
||||
|
||||
#
|
||||
# ejabberd
|
||||
#
|
||||
|
||||
field :ejabberd_enabled, type: :boolean,
|
||||
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
|
||||
default: ENV["EJABBERD_API_URL"].present?
|
||||
|
||||
field :ejabberd_api_url, type: :string,
|
||||
default: ENV["EJABBERD_API_URL"].presence
|
||||
@@ -75,7 +106,7 @@ class Setting < RailsSettings::Base
|
||||
default: ENV["GITEA_PUBLIC_URL"].presence
|
||||
|
||||
field :gitea_enabled, type: :boolean,
|
||||
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
|
||||
default: ENV["GITEA_PUBLIC_URL"].present?
|
||||
|
||||
#
|
||||
# Lightning Network
|
||||
@@ -85,16 +116,19 @@ class Setting < RailsSettings::Base
|
||||
default: ENV["LNDHUB_API_URL"].presence
|
||||
|
||||
field :lndhub_enabled, type: :boolean,
|
||||
default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
|
||||
default: ENV["LNDHUB_API_URL"].present?
|
||||
|
||||
field :lndhub_admin_token, type: :string,
|
||||
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
|
||||
|
||||
field :lndhub_admin_enabled, type: :boolean,
|
||||
default: (ENV["LNDHUB_ADMIN_UI"] || false)
|
||||
default: ENV["LNDHUB_ADMIN_UI"] || false
|
||||
|
||||
field :lndhub_public_key, type: :string,
|
||||
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
|
||||
|
||||
field :lndhub_keysend_enabled, type: :boolean,
|
||||
default: -> { self.lndhub_public_key.present?.to_s || false }
|
||||
default: -> { self.lndhub_public_key.present? }
|
||||
|
||||
#
|
||||
# Mastodon
|
||||
@@ -104,7 +138,10 @@ class Setting < RailsSettings::Base
|
||||
default: ENV["MASTODON_PUBLIC_URL"].presence
|
||||
|
||||
field :mastodon_enabled, type: :boolean,
|
||||
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
|
||||
default: ENV["MASTODON_PUBLIC_URL"].present?
|
||||
|
||||
field :mastodon_address_domain, type: :string,
|
||||
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
|
||||
|
||||
#
|
||||
# MediaWiki
|
||||
@@ -114,7 +151,7 @@ class Setting < RailsSettings::Base
|
||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
|
||||
|
||||
field :mediawiki_enabled, type: :boolean,
|
||||
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
|
||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
|
||||
|
||||
#
|
||||
# Nostr
|
||||
@@ -127,11 +164,37 @@ class Setting < RailsSettings::Base
|
||||
#
|
||||
|
||||
field :remotestorage_enabled, type: :boolean,
|
||||
default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
|
||||
default: ENV["RS_STORAGE_URL"].present?
|
||||
|
||||
field :rs_storage_url, type: :string,
|
||||
default: ENV["RS_STORAGE_URL"].presence
|
||||
|
||||
field :rs_redis_url, type: :string,
|
||||
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
|
||||
|
||||
|
||||
#
|
||||
# E-Mail Service
|
||||
#
|
||||
|
||||
field :email_enabled, type: :boolean,
|
||||
default: ENV["EMAIL_SMTP_HOST"].present?
|
||||
|
||||
# field :email_smtp_host, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_HOST"].presence
|
||||
#
|
||||
# field :email_smtp_port, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
|
||||
#
|
||||
# field :email_smtp_enable_starttls, type: :string,
|
||||
# default: ENV["EMAIL_SMTP_PORT"].presence || true
|
||||
#
|
||||
# field :email_auth_method, type: :string,
|
||||
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
|
||||
#
|
||||
# field :email_imap_host, type: :string,
|
||||
# default: ENV["EMAIL_IMAP_HOST"].presence
|
||||
#
|
||||
# field :email_imap_port, type: :string,
|
||||
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
|
||||
end
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
require 'nostr'
|
||||
|
||||
class User < ApplicationRecord
|
||||
include EmailValidatable
|
||||
|
||||
attr_accessor :display_name
|
||||
attr_accessor :avatar_new
|
||||
attr_accessor :current_password
|
||||
|
||||
serialize :preferences, UserPreferences
|
||||
serialize :preferences, coder: UserPreferences
|
||||
|
||||
#
|
||||
# Relations
|
||||
#
|
||||
|
||||
has_many :invitations, dependent: :destroy
|
||||
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
|
||||
has_one :inviter, through: :invitation, source: :user
|
||||
@@ -20,6 +27,10 @@ class User < ApplicationRecord
|
||||
|
||||
has_many :remote_storage_authorizations
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
|
||||
validates_uniqueness_of :cn, scope: :ou
|
||||
validates_length_of :cn, minimum: 3
|
||||
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
||||
@@ -30,7 +41,8 @@ class User < ApplicationRecord
|
||||
message: "is invalid. Usernames need to start with a letter."
|
||||
# FIXME This needs a server restart to apply values
|
||||
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
|
||||
message: "has already been taken"
|
||||
message: "has already been taken",
|
||||
unless: Proc.new{ |u| u.persisted? }
|
||||
|
||||
validates_uniqueness_of :email
|
||||
validates :email, email: true
|
||||
@@ -40,10 +52,20 @@ class User < ApplicationRecord
|
||||
|
||||
validates_uniqueness_of :nostr_pubkey, allow_blank: true
|
||||
|
||||
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
|
||||
|
||||
# Include default devise modules. Others available are:
|
||||
@@ -70,12 +92,14 @@ class User < ApplicationRecord
|
||||
def devise_after_confirmation
|
||||
if ldap_entry[:mail] != self.email
|
||||
# E-Mail update confirmed
|
||||
LdapManager::UpdateEmail.call(self.dn, self.email)
|
||||
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
|
||||
else
|
||||
# E-Mail from signup confirmed (i.e. account activation)
|
||||
|
||||
# TODO Make configurable, only activate globally enabled services
|
||||
enable_service %w[ discourse gitea mediawiki xmpp ]
|
||||
|
||||
#TODO enable in development when we have easy setup of ejabberd etc.
|
||||
# TODO enable in development when we have easy setup of ejabberd etc.
|
||||
return if Rails.env.development? || !Setting.ejabberd_enabled?
|
||||
|
||||
XmppExchangeContactsJob.perform_later(inviter, self) if inviter.present?
|
||||
@@ -109,6 +133,11 @@ class User < ApplicationRecord
|
||||
"#{self.cn}@#{self.ou}"
|
||||
end
|
||||
|
||||
def mastodon_address
|
||||
return nil unless Setting.mastodon_enabled?
|
||||
"#{self.cn}@#{Setting.mastodon_address_domain}"
|
||||
end
|
||||
|
||||
def valid_attribute?(attribute_name)
|
||||
self.valid?
|
||||
self.errors[attribute_name].blank?
|
||||
@@ -134,6 +163,10 @@ class User < ApplicationRecord
|
||||
@display_name ||= ldap_entry[:display_name]
|
||||
end
|
||||
|
||||
def avatar
|
||||
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
|
||||
end
|
||||
|
||||
def services_enabled
|
||||
ldap_entry[:service] || []
|
||||
end
|
||||
@@ -156,10 +189,28 @@ class User < ApplicationRecord
|
||||
ldap.delete_attribute(dn,:service)
|
||||
end
|
||||
|
||||
def nostr_pubkey_bech32
|
||||
return nil unless nostr_pubkey.present?
|
||||
Nostr::PublicKey.new(nostr_pubkey).to_bech32
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap
|
||||
return @ldap_service if defined?(@ldap_service)
|
||||
@ldap_service = LdapService.new
|
||||
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
|
||||
|
||||
63
app/services/app_catalog_manager/update_metadata.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
require "manifique"
|
||||
require "down"
|
||||
|
||||
module AppCatalogManager
|
||||
class UpdateMetadata < AppCatalogManagerService
|
||||
def initialize(app:)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call
|
||||
agent = Manifique::Agent.new(url: @app.url)
|
||||
metadata = agent.fetch_metadata
|
||||
|
||||
@app.name = metadata.name
|
||||
|
||||
[:name, :short_name, :description, :theme_color, :background_color,
|
||||
:display, :start_url, :scope, :share_target, :icons].each do |prop|
|
||||
@app.metadata[prop] = metadata.send(prop) if prop
|
||||
end
|
||||
|
||||
@app.save!
|
||||
|
||||
# TODO move icon downloads to separate, async job
|
||||
|
||||
if icon = metadata.select_icon(sizes: "256x256") ||
|
||||
icon = metadata.select_icon(sizes: "192x192")
|
||||
attach_remote_image(:icon, icon)
|
||||
# TODO elsif get whatever is available
|
||||
end
|
||||
|
||||
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
|
||||
attach_remote_image(:apple_touch_icon, apple_touch_icon)
|
||||
end
|
||||
rescue Manifique::Error => e
|
||||
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
|
||||
Rails.logger.warn(msg)
|
||||
Sentry.capture_message(msg) if Setting.sentry_enabled?
|
||||
false
|
||||
end
|
||||
|
||||
def attach_remote_image(attachment_name, icon)
|
||||
if icon['src'].start_with?("http")
|
||||
download_url = icon['src']
|
||||
else
|
||||
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
|
||||
end
|
||||
filename = "#{attachment_name}-#{Time.now.to_i}.png"
|
||||
key = "web_apps/#{@app.id}/icons/#{filename}"
|
||||
|
||||
begin
|
||||
tempfile = Down.download(download_url)
|
||||
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
|
||||
rescue Down::NotFound
|
||||
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
|
||||
Rails.logger.warn(msg)
|
||||
Sentry.capture_message(msg)
|
||||
rescue => e
|
||||
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
2
app/services/app_catalog_manager_service.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
class AppCatalogManagerService < ApplicationService
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
class ApplicationService
|
||||
# This enables executing a service's `#call` method directly via
|
||||
# `MyService.call(args)`, without creating a class instance it first.
|
||||
def self.call(*args, &block)
|
||||
new(*args, &block).call
|
||||
def self.call(**args, &block)
|
||||
new(**args, &block).call
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,11 @@
|
||||
module BtcpayManager
|
||||
class FetchLightningWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/lightning/BTC/balance"
|
||||
|
||||
{
|
||||
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/services/btcpay_manager/fetch_onchain_wallet_balance.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
24
app/services/btcpay_manager_service.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,11 +1,11 @@
|
||||
class CreateAccount < ApplicationService
|
||||
def initialize(args)
|
||||
@username = args[:username]
|
||||
@domain = args[:ou] || Setting.primary_domain
|
||||
@email = args[:email]
|
||||
@password = args[:password]
|
||||
@invitation = args[:invitation]
|
||||
@confirmed = args[:confirmed]
|
||||
def initialize(account:)
|
||||
@username = account[:username]
|
||||
@domain = account[:ou] || Setting.primary_domain
|
||||
@email = account[:email]
|
||||
@password = account[:password]
|
||||
@invitation = account[:invitation]
|
||||
@confirmed = account[:confirmed]
|
||||
end
|
||||
|
||||
def call
|
||||
|
||||
17
app/services/create_invitations.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class CreateInvitations < ApplicationService
|
||||
def initialize(user:, amount:, notify: true)
|
||||
@user = user
|
||||
@amount = amount
|
||||
@notify = notify
|
||||
end
|
||||
|
||||
def call
|
||||
@amount.times do
|
||||
Invitation.create(user: @user)
|
||||
end
|
||||
|
||||
if @notify
|
||||
NotificationMailer.with(user: @user).new_invitations_available.deliver_later
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/services/ldap_manager/fetch_avatar.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module LdapManager
|
||||
class FetchAvatar < LdapManagerService
|
||||
def initialize(cn:)
|
||||
@cn = cn
|
||||
end
|
||||
|
||||
def call
|
||||
treebase = 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
|
||||
27
app/services/ldap_manager/update_avatar.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -1,6 +1,6 @@
|
||||
module LdapManager
|
||||
class UpdateDisplayName < LdapManagerService
|
||||
def initialize(dn, display_name)
|
||||
def initialize(dn:, display_name:)
|
||||
@dn = dn
|
||||
@display_name = display_name
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module LdapManager
|
||||
class UpdateEmail < LdapManagerService
|
||||
def initialize(dn, address)
|
||||
def initialize(dn:, address:)
|
||||
@dn = dn
|
||||
@address = address
|
||||
end
|
||||
|
||||
12
app/services/ldap_manager/update_email_maildrop.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module LdapManager
|
||||
class UpdateEmailMaildrop < LdapManagerService
|
||||
def initialize(dn:, address:)
|
||||
@dn = dn
|
||||
@address = address
|
||||
end
|
||||
|
||||
def call
|
||||
replace_attribute @dn, :mailRoutingAddress, @address
|
||||
end
|
||||
end
|
||||
end
|
||||
12
app/services/ldap_manager/update_email_password.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module LdapManager
|
||||
class UpdateEmailPassword < LdapManagerService
|
||||
def initialize(dn:, password_hash:)
|
||||
@dn = dn
|
||||
@password_hash = password_hash
|
||||
end
|
||||
|
||||
def call
|
||||
replace_attribute @dn, :mailpassword, @password_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2 +1,5 @@
|
||||
class LdapManagerService < LdapService
|
||||
def suffix
|
||||
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,8 +50,11 @@ class LdapService < ApplicationService
|
||||
treebase = ldap_config["base"]
|
||||
end
|
||||
|
||||
attributes = %w{dn cn uid mail displayName admin service}
|
||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||
attributes = %w[
|
||||
dn cn uid mail displayName admin service
|
||||
mailRoutingAddress mailpassword
|
||||
]
|
||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||
|
||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries.sort_by! { |e| e.cn[0] }
|
||||
@@ -61,7 +64,9 @@ class LdapService < ApplicationService
|
||||
mail: e.try(:mail) ? e.mail.first : nil,
|
||||
display_name: e.try(:displayName) ? e.displayName.first : nil,
|
||||
admin: e.try(:admin) ? 'admin' : nil,
|
||||
service: e.try(:service)
|
||||
service: e.try(:service),
|
||||
email_maildrop: e.try(:mailRoutingAddress),
|
||||
email_password: e.try(:mailpassword)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class LndhubV2 < Lndhub
|
||||
end
|
||||
|
||||
def create_account(payload={})
|
||||
post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
|
||||
post "v2/users", payload, admin_token: Setting.lndhub_admin_token
|
||||
end
|
||||
|
||||
def create_invoice(payload)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module NostrManager
|
||||
class ValidateId < NostrManagerService
|
||||
def initialize(event)
|
||||
def initialize(event:)
|
||||
@event = Nostr::Event.new(**event)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module NostrManager
|
||||
class VerifySignature < NostrManagerService
|
||||
def initialize(event)
|
||||
def initialize(event:)
|
||||
@event = Nostr::Event.new(**event)
|
||||
end
|
||||
|
||||
|
||||
7
app/services/router.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class Router
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def self.default_url_options
|
||||
ActionMailer::Base.default_url_options
|
||||
end
|
||||
end
|
||||
56
app/views/admin/app_catalog/web_apps/index.html.erb
Normal file
@@ -0,0 +1,56 @@
|
||||
<%= render HeaderComponent.new(title: "App Catalog") %>
|
||||
|
||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_app_catalog') do %>
|
||||
<section>
|
||||
<%= render QuickstatsContainerComponent.new do %>
|
||||
<%= render QuickstatsItemComponent.new(
|
||||
type: :number,
|
||||
title: 'Known Web Apps',
|
||||
value: @stats[:known_apps],
|
||||
) %>
|
||||
<%# <%= render QuickstatsItemComponent.new(
|
||||
<%# type: :number,
|
||||
<%# title: 'Accepted',
|
||||
<%# value: @stats[:accepted],
|
||||
<%# ) %>
|
||||
<%# <%= render QuickstatsItemComponent.new(
|
||||
<%# type: :number,
|
||||
<%# title: 'Users with referrals',
|
||||
<%# value: @stats[:users_with_referrals],
|
||||
<%# meta: "/ #{User.count}"
|
||||
<%# ) %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% if @web_apps.any? %>
|
||||
<section>
|
||||
<h3>Web Apps</h3>
|
||||
<table class="divided mb-8">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>URL</th>
|
||||
<th class="hidden md:table-cell">RS Auths</th>
|
||||
<th class="hidden md:table-cell">Created at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @web_apps.each do |web_app| %>
|
||||
<tr>
|
||||
<td><%= web_app.name %></td>
|
||||
<td><%= link_to web_app.url, web_app.url,
|
||||
target: "_blank", rel: "nofollow noopener",
|
||||
class: "ks-text-link" %></td>
|
||||
<td class="hidden md:table-cell"><%= web_app.remote_storage_authorizations.count %></td>
|
||||
<td class="hidden md:table-cell">
|
||||
<span title="<%= web_app.created_at %>" class="cursor-help">
|
||||
<%= time_ago_in_words web_app.created_at, include_seconds: false %> ago
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<%== pagy_nav @pagy %>
|
||||
</section>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,7 +1,7 @@
|
||||
<%= render HeaderComponent.new(title: "Settings") %>
|
||||
|
||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
|
||||
<%= form_for(Setting.new, url: admin_settings_registrations_path, method: :put) do |f| %>
|
||||
<section>
|
||||
<h3>Registrations</h3>
|
||||
|
||||
37
app/views/admin/settings/services/_btcpay.html.erb
Normal file
@@ -0,0 +1,37 @@
|
||||
<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>
|
||||
16
app/views/admin/settings/services/_droneci.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
16
app/views/admin/settings/services/_email.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h3>E-Mail</h3>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
attribute: :email_enabled,
|
||||
enabled: Setting.email_enabled?,
|
||||
title: "Enable E-Mail service integration",
|
||||
description: "Enable/configure LDAP attributes for use with a mail server"
|
||||
) %>
|
||||
<%# <% if Setting.email_enabled? %>
|
||||
<%# <%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
<%# key: :gitea_public_url,
|
||||
<%# title: "Public URL"
|
||||
<%# ) %>
|
||||
<%# <% end %>
|
||||
</ul>
|
||||
@@ -9,29 +9,35 @@
|
||||
) %>
|
||||
<% if Setting.lndhub_enabled? %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :lndhub_api_url,
|
||||
title: "API URL"
|
||||
) %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
attribute: :lndhub_admin_enabled,
|
||||
enabled: Setting.lndhub_admin_enabled?,
|
||||
title: "Enable LNDHub admin panel",
|
||||
description: "LNDHub database configuration present and admin panel enabled"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
attribute: :lndhub_keysend_enabled,
|
||||
enabled: Setting.lndhub_keysend_enabled?,
|
||||
title: "Enable keysend payments",
|
||||
description: "Allow users to receive invoice-less payments to their Lightning Address"
|
||||
) %>
|
||||
<% if Setting.lndhub_keysend_enabled? %>
|
||||
key: :lndhub_api_url,
|
||||
title: "API URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :lndhub_public_key,
|
||||
title: "Public key",
|
||||
description: "The public key of the Lightning node used by LNDHub"
|
||||
) %>
|
||||
key: :lndhub_admin_token,
|
||||
type: :password,
|
||||
title: "Admin token",
|
||||
description: "Auth token for creating new lndhub accounts"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
attribute: :lndhub_admin_enabled,
|
||||
enabled: Setting.lndhub_admin_enabled?,
|
||||
title: "Enable LNDHub admin panel",
|
||||
description: "LNDHub database configuration present and admin panel enabled"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
attribute: :lndhub_keysend_enabled,
|
||||
enabled: Setting.lndhub_keysend_enabled?,
|
||||
title: "Enable keysend payments",
|
||||
description: "Allow users to receive invoice-less payments to their Lightning Address"
|
||||
) %>
|
||||
<% if Setting.lndhub_keysend_enabled? %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :lndhub_public_key,
|
||||
title: "Public key",
|
||||
description: "The public key of the Lightning node used by LNDHub"
|
||||
) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -12,5 +12,9 @@
|
||||
key: :mastodon_public_url,
|
||||
title: "Public URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :mastodon_address_domain,
|
||||
title: "User address domain"
|
||||
) %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<%= render HeaderComponent.new(title: "Settings") %>
|
||||
|
||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
|
||||
<%= hidden_field_tag :service, @service %>
|
||||
|
||||
<%= form_for(Setting.new, url: admin_settings_service_path(@service), method: :put) do |f| %>
|
||||
<% if @errors && @errors.any? %>
|
||||
<section>
|
||||
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
||||
21
app/views/admin/users/_create_invitations.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<h3>Add new invitations to <%= @user.cn %>'s account</h3>
|
||||
<%= form_with(url: invitations_admin_user_path, method: :post) do |form| %>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
positioning: :horizontal,
|
||||
title: "Amount"
|
||||
) do %>
|
||||
<%= form.select :amount, options_for_select([
|
||||
["3", "3"], ["5", "5"], ["10", "10"], ["20", "20"]
|
||||
]) %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
field_name: "notify_user",
|
||||
enabled: true,
|
||||
title: "Notify user via email"
|
||||
) %>
|
||||
</ul>
|
||||
<p class="pt-6 border-t border-gray-200 text-right">
|
||||
<%= form.submit 'Add', class: "btn-md btn-blue w-full" %>
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= render HeaderComponent.new(title: "Users: #{@ou}") %>
|
||||
<%= render HeaderComponent.new(title: "Users") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
@@ -16,19 +16,6 @@
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<% if @orgs.length > 1 %>
|
||||
<section>
|
||||
<h3 class="hidden">Domains</h3>
|
||||
<ul>
|
||||
<% @orgs.each do |org| %>
|
||||
<li class="inline-block">
|
||||
<%= link_to org[:ou], admin_users_path(ou: org[:ou]), class: "ks-text-link" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<table class="divided mb-8">
|
||||
<thead>
|
||||
@@ -36,13 +23,12 @@
|
||||
<th>UID</th>
|
||||
<th>Status</th>
|
||||
<th>Roles</th>
|
||||
<!-- <th>Password</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td><%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %></td>
|
||||
<td><%= link_to(user.cn, admin_user_path(user.cn), class: 'ks-text-link') %></td>
|
||||
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
|
||||
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= render HeaderComponent.new(title: "User: #{@user.address}") %>
|
||||
<%= render HeaderComponent.new(title: "User: #{@user.cn}") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
|
||||
@@ -42,8 +42,34 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Invitations available</th>
|
||||
<td>
|
||||
<%= @user.invitations.count %>
|
||||
<td data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<div class="flex justify-between">
|
||||
<span>
|
||||
<%= @user.invitations.count %>
|
||||
</span>
|
||||
<span>
|
||||
<button id="add-invitations" data-action="click->modal#open">
|
||||
<%= render partial: "icons/plus-circle", locals: {
|
||||
custom_class: "text-green-600 hover:text-green-500 -mt-2 -mb-1 h-6 w-6 inline-block"
|
||||
} %>
|
||||
</button>
|
||||
<% if @user.invitations.unused.count > 0 %>
|
||||
<%= link_to invitations_admin_user_path(@user.cn),
|
||||
id: "remove-invitations", data: {
|
||||
turbo_method: :delete,
|
||||
turbo_confirm: "Delete all of #{@user.cn}'s available invitations?"
|
||||
} do %>
|
||||
<%= render partial: "icons/x-circle", locals: {
|
||||
custom_class: "text-red-600 hover:text-red-500 -mt-2 -mb-1 h-6 w-6 inline-block"
|
||||
} %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
||||
<%= render partial: "admin/users/create_invitations",
|
||||
locals: { user: @user } %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -63,6 +89,12 @@
|
||||
</section>
|
||||
|
||||
<section class="sm:flex-1 sm:pt-0">
|
||||
<% if @avatar.present? %>
|
||||
<h3>LDAP<h3>
|
||||
<p>
|
||||
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
|
||||
</p>
|
||||
<% end %>
|
||||
<!-- <h3>Actions</h3> -->
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -21,17 +21,17 @@
|
||||
</p>
|
||||
<p>
|
||||
A good way to get started is to join one of our
|
||||
<a href="https://community.kosmos.org/t/kosmos-weekly-call/36" target="_blank" class="ks-text-link">weekly calls</a>
|
||||
<a href="https://wiki.kosmos.org/Main_Page#Chat" target="_blank" class="ks-text-link">chat rooms</a>
|
||||
and introduce yourself. Alternatively, you can also ping us on any other
|
||||
medium, or even just grab an open issue on
|
||||
<a href="https://github.com/67P/" target="_blank" class="ks-text-link">GitHub</a>
|
||||
or our
|
||||
medium, or even just grab an open issue on our
|
||||
<a href="https://gitea.kosmos.org/kosmos/" target="_blank" class="ks-text-link">Gitea</a>
|
||||
and dive right in (be sure to comment first, to prevent double efforts).
|
||||
or on
|
||||
<a href="https://github.com/67P/" target="_blank" class="ks-text-link">GitHub</a>
|
||||
and dive right in.
|
||||
</p>
|
||||
<p class="mb-8">
|
||||
Last but not least, if you want to help by proposing new features or
|
||||
services, head over to the
|
||||
services, or by giving feedback on existing ones, head over to the
|
||||
<a href="https://community.kosmos.org/" target="_blank" class="ks-text-link">community forums</a>,
|
||||
where you can do just that.
|
||||
</p>
|
||||
@@ -43,7 +43,7 @@
|
||||
</p>
|
||||
<p>
|
||||
We have run two 6-month trials so far, with the next trial period
|
||||
starting sometime in Q1 2023. Watch your email for notifications about it!
|
||||
starting sometime in Q1 2024. Watch your email for notifications about it!
|
||||
</p>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -2,78 +2,71 @@
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<p class="mb-8">
|
||||
Your Kosmos account and password currently give you access to these
|
||||
services:
|
||||
</p>
|
||||
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-[center_top_-50px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_xmpp.svg)]">
|
||||
<%= link_to "https://wiki.kosmos.org/Services:Chat",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Chat</h3>
|
||||
<p class="text-gray-600">
|
||||
Federated chat rooms and instant messaging
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-[length:95%] bg-center bg-no-repeat
|
||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Discourse</h3>
|
||||
<p class="text-gray-600">
|
||||
Kosmos community forums and user support/help site
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-[center_top_-20px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_mediawiki.svg)]">
|
||||
<%= link_to "https://wiki.kosmos.org",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Wiki</h3>
|
||||
<p class="text-gray-600">
|
||||
Kosmos documentation and knowledge base
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_lightning.svg)]">
|
||||
<%= link_to services_lightning_index_path,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Lightning Network</h3>
|
||||
<p class="text-gray-600">
|
||||
Send and receive sats over the Bitcoin Lightning Network
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-center bg-no-repeat
|
||||
bg-[url(/img/logos/icon_gitea.png)]">
|
||||
<%= link_to "https://gitea.kosmos.org",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Gitea</h3>
|
||||
<p class="text-gray-600">
|
||||
Code hosting and collaboration for software projects
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-[center_top_-70px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_droneci.svg)]">
|
||||
<%= link_to "https://drone.kosmos.org",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Drone CI</h3>
|
||||
<p class="text-gray-600">
|
||||
Continuous integration for software projects on Gitea
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if Setting.remotestorage_enabled? && Flipper.enabled?(:remotestorage, current_user) %>
|
||||
<% if Setting.ejabberd_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-[center_top_-50px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_xmpp.svg)]">
|
||||
<%= link_to services_chat_path,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Chat</h3>
|
||||
<p class="text-gray-600">
|
||||
Federated chat rooms and instant messaging
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.mastodon_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-[length:80%] bg-[right_top_-30px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_mastodon.svg)]">
|
||||
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Mastodon</h3>
|
||||
<p class="text-gray-600">
|
||||
Your account on the Open Social Web
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.email_enabled? &&
|
||||
Flipper.enabled?(:email, current_user) %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
||||
<%= link_to services_email_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">E-Mail</h3>
|
||||
<p class="text-gray-600">
|
||||
A no-bullshit email account
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.discourse_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-[length:95%] bg-center bg-no-repeat
|
||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Discourse</h3>
|
||||
<p class="text-gray-600">
|
||||
Kosmos community forums and user support/help site
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.lndhub_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_lightning.svg)]">
|
||||
<%= link_to services_lightning_index_path,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Lightning Network</h3>
|
||||
<p class="text-gray-600">
|
||||
Send and receive sats over the Bitcoin Lightning Network
|
||||
</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">
|
||||
<%= link_to services_storage_path,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
@@ -84,16 +77,45 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- <div class="border border-gray-300 rounded-md hover:border-gray-400 -->
|
||||
<!-- bg-[length:80%] bg-[right_top_-30px] bg-no-repeat -->
|
||||
<!-- bg-[url(/img/logos/icon_mastodon.svg)]"> -->
|
||||
<!-- <%= link_to "https://kosmos.social", class: "block h-full px-6 py-6 rounded-md" do %> -->
|
||||
<!-- <h3 class="mb-3.5">Mastodon</h3> -->
|
||||
<!-- <p class="text-gray-400"> -->
|
||||
<!-- Your account on the Open Social Web -->
|
||||
<!-- </p> -->
|
||||
<!-- <% end %> -->
|
||||
<!-- </div> -->
|
||||
<% if Setting.gitea_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-center bg-no-repeat
|
||||
bg-[url(/img/logos/icon_gitea.png)]">
|
||||
<%= link_to Setting.gitea_public_url,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Gitea</h3>
|
||||
<p class="text-gray-600">
|
||||
Code hosting and collaboration for software projects
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if Setting.droneci_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-[center_top_-70px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_droneci.svg)]">
|
||||
<%= link_to 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.mediawiki_enabled? %>
|
||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||
bg-cover bg-[center_top_-20px] bg-no-repeat
|
||||
bg-[url(/img/logos/icon_mediawiki.svg)]">
|
||||
<%= link_to Setting.mediawiki_public_url,
|
||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||
<h3 class="mb-3.5">Wiki</h3>
|
||||
<p class="text-gray-600">
|
||||
Kosmos documentation and knowledge base
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<%
|
||||
# TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed
|
||||
enable_turbo = !session[:user_return_to] || !session[:user_return_to].match?('/discourse/connect')
|
||||
enable_turbo = session[:user_return_to].blank? ||
|
||||
['/discourse/connect', '/rs/oauth'].none? { |s| session[:user_return_to].match(s) }
|
||||
%>
|
||||
|
||||
<%= render HeaderCompactComponent.new(title: "Log in") %>
|
||||
@@ -12,7 +13,8 @@
|
||||
<div class="mb-6">
|
||||
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
||||
<p class="flex gap-2 items-center">
|
||||
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
||||
<%= f.text_field :cn, value: h(params[:cn]),
|
||||
autofocus: params[:cn].blank?, autocomplete: "username",
|
||||
required: true, class: "relative grow", tabindex: "1" %>
|
||||
<span class="relative shrink-0 text-gray-500">@ <%= Setting.primary_domain %></span>
|
||||
</p>
|
||||
@@ -20,7 +22,8 @@
|
||||
<p class="mb-8">
|
||||
<%= f.label :password, class: 'block mb-2 font-bold' %>
|
||||
<%= f.password_field :password, autocomplete: "current-password",
|
||||
required: true, class: "w-full", tabindex: "2" %>
|
||||
autofocus: params[:cn].present?, required: true,
|
||||
class: "w-full", tabindex: "2" %>
|
||||
</p>
|
||||
|
||||
<%= tag.div class: "flex items-center mb-8 gap-x-3", data: {
|
||||
|
||||
@@ -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-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></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-globe <%= custom_class %>"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 430 B |
10
app/views/icons/_kebap-menu.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" class="<%= custom_class %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Menu</title>
|
||||
<g id="kebap-menu" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<rect id="Container" x="0" y="0" width="24" height="24"></rect>
|
||||
<path d="M12,6 C12.5522847,6 13,5.55228475 13,5 C13,4.44771525 12.5522847,4 12,4 C11.4477153,4 11,4.44771525 11,5 C11,5.55228475 11.4477153,6 12,6 Z" stroke="#030819" stroke-width="2" stroke-linecap="round" stroke-dasharray="0,0"></path>
|
||||
<path d="M12,13 C12.5522847,13 13,12.5522847 13,12 C13,11.4477153 12.5522847,11 12,11 C11.4477153,11 11,11.4477153 11,12 C11,12.5522847 11.4477153,13 12,13 Z" stroke="#030819" stroke-width="2" stroke-linecap="round" stroke-dasharray="0,0"></path>
|
||||
<path d="M12,20 C12.5522847,20 13,19.5522847 13,19 C13,18.4477153 12.5522847,18 12,18 C11.4477153,18 11,18.4477153 11,19 C11,19.5522847 11.4477153,20 12,20 Z" stroke="#030819" stroke-width="2" stroke-linecap="round" stroke-dasharray="0,0"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></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-mail <%= custom_class %>"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 375 B |
@@ -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-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></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-plus-circle <%= custom_class %>"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 372 B |
6
app/views/icons/_remotestorage.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" class="icon-remotestorage <%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-66.822 -.16484)">
|
||||
<polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 867 B |
@@ -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-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></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-star <%= custom_class %>"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 360 B |
@@ -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-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></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-x-circle <%= custom_class %>"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 367 B |
@@ -8,7 +8,9 @@
|
||||
</p>
|
||||
<ul class="md:w-3/4">
|
||||
<% @invitations_unused.each do |invitation| %>
|
||||
<li class="mb-3 flex gap-1" data-controller="clipboard modal">
|
||||
<li class="mb-3 flex gap-1"
|
||||
data-controller="clipboard modal"
|
||||
data-action="keydown.esc->modal#close">
|
||||
<input type="text" disabled class="relative grow font-mono"
|
||||
value="<%= invitation_url(invitation.token) %>"
|
||||
data-clipboard-target="source" />
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
<div class="pt-4 pb-3 border-t border-gray-200/10">
|
||||
<div class="px-5 text-base font-normal text-white">
|
||||
<%= current_user.address %></strong>
|
||||
<%= current_user.address %>
|
||||
</div>
|
||||
<div class="mt-3 px-2 space-y-1">
|
||||
<%= link_to "Log out", destroy_user_session_path,
|
||||
|
||||