Compare commits
130 Commits
v0.7.0
...
aab6793b86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aab6793b86
|
||
|
|
cfd0935bdc
|
||
|
|
c2dae105ff
|
||
|
|
2a70bf2fb9
|
||
|
|
9a9947f9ad
|
||
|
|
bdf5a18ad4
|
||
|
|
aa399b862a
|
||
|
|
713e91a720
|
||
|
|
8ec2a6d7e4
|
||
|
|
4ecf2c4246
|
||
|
|
4fdf8accd6
|
||
|
|
f451adcb53
|
||
|
|
721dccb499
|
||
|
|
27bb7d1bfe
|
||
|
|
1d44181fb5
|
||
|
|
de67f59d5c
|
||
|
|
1995e6dda2
|
||
|
|
600cfe0f78
|
||
|
|
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
|
||
|
|
fcf9a065e1
|
||
|
|
ec9bcacd46
|
||
|
|
645abac810 | ||
|
|
e11be727a1 | ||
|
|
12b24337e7 | ||
|
|
b0bfc290c4 | ||
|
|
4c6c81171b | ||
|
|
4d88a40109
|
||
|
|
d9b39b36fb
|
||
|
|
06aed8c33d
|
||
| 0a778e92d8 | |||
|
|
e5a5633e44
|
||
|
|
a68825493f
|
||
|
|
e1e83386a8
|
||
|
|
3adc1917f6
|
||
|
|
8a570ce724
|
||
|
|
c78df9e5f1
|
||
|
5c2df3df07
|
|||
| 83e3e2ecd8 | |||
| b32e2fcb7b | |||
| 96a4db5bae | |||
| c7925f132e | |||
| e4406bf6ff | |||
| ee7769c8c7 | |||
| fdf3218f88 | |||
| 652ed5f7e3 | |||
|
|
e4ed797920
|
||
|
|
93740f17ef
|
||
|
|
affb058671
|
||
|
716d4b944a
|
|||
|
42af148168
|
|||
|
|
dabd892a25
|
||
|
|
eeabbdb7df
|
||
|
ee42d68471
|
|||
|
7acc3b2106
|
|||
|
20c014607c
|
@@ -17,9 +17,11 @@ steps:
|
||||
branch:
|
||||
- master
|
||||
- name: rspec
|
||||
image: guildeducation/rails:2.7.2-14.20.0
|
||||
image: gitea.kosmos.org/kosmos/akkounts-ci:0.1.0
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
RS_REDIS_URL: redis://redis:6379/1
|
||||
commands:
|
||||
- bundle config unset deployment
|
||||
- bundle config set cache_all 'true'
|
||||
@@ -42,6 +44,10 @@ steps:
|
||||
branch:
|
||||
- master
|
||||
|
||||
services:
|
||||
- name: redis
|
||||
image: redis
|
||||
|
||||
volumes:
|
||||
- name: cache
|
||||
host:
|
||||
|
||||
13
.env.example
@@ -10,6 +10,14 @@ SMTP_DOMAIN=example.com
|
||||
SMTP_AUTH_METHOD=plain
|
||||
SMTP_ENABLE_STARTTLS=auto
|
||||
|
||||
# S3_ENABLED=true
|
||||
# S3_ENDPOINT=https://s3.kosmos.org
|
||||
# S3_REGION=garage
|
||||
# S3_BUCKET=akkounts-production
|
||||
# S3_ALIAS_HOST=accounts.s3.kosmos.org
|
||||
# S3_ACCESS_KEY=123456abcdefg
|
||||
# S3_SECRET_KEY=123456789123456789123456789
|
||||
|
||||
LDAP_HOST=localhost
|
||||
LDAP_PORT=389
|
||||
LDAP_ADMIN_PASSWORD=passthebutter
|
||||
@@ -22,15 +30,20 @@ WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||
|
||||
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
||||
|
||||
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'
|
||||
|
||||
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=''
|
||||
|
||||
LNDHUB_API_URL='http://localhost:3023'
|
||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
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'
|
||||
|
||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
RS_REDIS_URL='redis://localhost:6379/1'
|
||||
|
||||
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
|
||||
|
||||
@@ -7,6 +7,7 @@ version-resolver:
|
||||
minor:
|
||||
labels:
|
||||
- 'release/minor'
|
||||
- 'feature'
|
||||
patch:
|
||||
labels:
|
||||
- 'release/patch'
|
||||
|
||||
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
|
||||
|
||||
@@ -4,14 +4,14 @@ FROM ruby:2.7.6
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
|
||||
ldap-utils tini
|
||||
ldap-utils tini libvips
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
WORKDIR /akkounts
|
||||
COPY Gemfile /akkounts/Gemfile
|
||||
COPY Gemfile.lock /akkounts/Gemfile.lock
|
||||
COPY package.json /akkounts/package.json
|
||||
|
||||
COPY ["Gemfile", "Gemfile.lock", "package.json", "./"]
|
||||
|
||||
RUN bundle install
|
||||
RUN gem install foreman
|
||||
RUN npm install -g yarn
|
||||
|
||||
5
Gemfile
@@ -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,12 +61,14 @@ gem "sentry-rails"
|
||||
# Services
|
||||
gem 'discourse_api'
|
||||
gem "lnurl"
|
||||
gem 'manifique', git: 'https://gitea.kosmos.org/5apps/manifique.git', branch: 'master'
|
||||
gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat'
|
||||
|
||||
group :development, :test do
|
||||
# Use sqlite3 as the database for Active Record
|
||||
gem 'sqlite3', '~> 1.4'
|
||||
gem 'rspec-rails'
|
||||
gem 'rails-controller-testing'
|
||||
gem "byebug", "~> 11.1"
|
||||
end
|
||||
|
||||
|
||||
283
Gemfile.lock
@@ -1,3 +1,13 @@
|
||||
GIT
|
||||
remote: https://gitea.kosmos.org/5apps/manifique.git
|
||||
revision: 8d79113438ee7c3e4288f840a135622519cffd5c
|
||||
branch: master
|
||||
specs:
|
||||
manifique (0.1.0)
|
||||
faraday (~> 2.7.11)
|
||||
faraday-follow_redirects (= 0.3.0)
|
||||
nokogiri (~> 1.15.4)
|
||||
|
||||
GIT
|
||||
remote: https://gitea.kosmos.org/kosmos/nostr-gem.git
|
||||
revision: 596529d9eb50d13b3f385245636698fccf37b442
|
||||
@@ -14,82 +24,100 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actioncable (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
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)
|
||||
actionmailbox (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
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.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
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)
|
||||
actionpack (7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
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)
|
||||
actiontext (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionview (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
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)
|
||||
activejob (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
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.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activestorage (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.5)
|
||||
activesupport (7.0.8)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.4)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.839.0)
|
||||
aws-sdk-core (3.185.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.72.0)
|
||||
aws-sdk-core (~> 3, >= 3.184.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.136.0)
|
||||
aws-sdk-core (~> 3, >= 3.181.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
backport (1.2.0)
|
||||
bcrypt (3.1.18)
|
||||
bech32 (1.3.0)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.19)
|
||||
bech32 (1.4.2)
|
||||
thor (>= 1.1.0)
|
||||
benchmark (0.2.1)
|
||||
bindex (0.8.1)
|
||||
bip-schnorr (0.6.0)
|
||||
ecdsa_ext (~> 0.5.0)
|
||||
brow (0.4.1)
|
||||
builder (3.2.4)
|
||||
byebug (11.1.3)
|
||||
capybara (3.39.2)
|
||||
@@ -107,7 +135,7 @@ GEM
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cssbundling-rails (1.1.2)
|
||||
cssbundling-rails (1.3.3)
|
||||
railties (>= 6.0.0)
|
||||
database_cleaner (2.0.2)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@@ -116,7 +144,7 @@ GEM
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.3)
|
||||
devise (4.9.2)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -135,6 +163,8 @@ GEM
|
||||
dotenv-rails (2.8.1)
|
||||
dotenv (= 2.8.1)
|
||||
railties (>= 3.2)
|
||||
down (5.4.1)
|
||||
addressable (~> 2.8)
|
||||
e2mmap (0.1.0)
|
||||
ecdsa (1.2.0)
|
||||
ecdsa_ext (0.5.0)
|
||||
@@ -149,9 +179,10 @@ GEM
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
faker (3.2.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.7.6)
|
||||
faraday (2.7.11)
|
||||
base64
|
||||
faraday-net_http (>= 2.0, < 3.1)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
@@ -159,41 +190,47 @@ GEM
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.0.2)
|
||||
faye-websocket (0.11.2)
|
||||
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.0.0)
|
||||
brow (~> 0.4.1)
|
||||
concurrent-ruby (< 2)
|
||||
flipper-active_record (0.28.0)
|
||||
flipper-active_record (1.0.0)
|
||||
activerecord (>= 4.2, < 8)
|
||||
flipper (~> 0.28.0)
|
||||
flipper-ui (0.28.0)
|
||||
flipper (~> 1.0.0)
|
||||
flipper-ui (1.0.0)
|
||||
erubi (>= 1.0.0, < 2.0.0)
|
||||
flipper (~> 0.28.0)
|
||||
rack (>= 1.4, < 3)
|
||||
flipper (~> 1.0.0)
|
||||
rack (>= 1.4, < 4)
|
||||
rack-protection (>= 1.5.3, <= 4.0.0)
|
||||
sanitize (< 7)
|
||||
fugit (1.8.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
hashdiff (1.0.1)
|
||||
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 (1.2.1)
|
||||
actionpack (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
jaro_winkler (1.5.6)
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
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 +243,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.0)
|
||||
loofah (2.21.4)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -220,10 +257,11 @@ GEM
|
||||
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)
|
||||
minitest (5.20.0)
|
||||
multipart-post (2.3.0)
|
||||
net-imap (0.3.6)
|
||||
net-imap (0.3.7)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.18.0)
|
||||
@@ -231,44 +269,51 @@ GEM
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
timeout
|
||||
net-smtp (0.3.3)
|
||||
net-smtp (0.4.0)
|
||||
net-protocol
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.2-x86_64-linux)
|
||||
nokogiri (1.15.4-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.4-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
pagy (6.0.4)
|
||||
pagy (6.1.0)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.3)
|
||||
parser (3.2.2.4)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.2.3)
|
||||
public_suffix (5.0.1)
|
||||
public_suffix (5.0.3)
|
||||
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
|
||||
rack (2.2.8)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
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)
|
||||
rails (7.0.8)
|
||||
actioncable (= 7.0.8)
|
||||
actionmailbox (= 7.0.8)
|
||||
actionmailer (= 7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actiontext (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.5)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
railties (= 7.0.8)
|
||||
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)
|
||||
@@ -276,9 +321,9 @@ 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)
|
||||
railties (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -290,13 +335,13 @@ GEM
|
||||
ffi (~> 1.0)
|
||||
rbs (2.8.4)
|
||||
redis (4.8.1)
|
||||
regexp_parser (2.8.1)
|
||||
responders (3.1.0)
|
||||
regexp_parser (2.8.2)
|
||||
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)
|
||||
@@ -306,7 +351,7 @@ 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)
|
||||
@@ -317,32 +362,36 @@ 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.57.1)
|
||||
base64 (~> 0.1.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.2.3)
|
||||
parser (>= 3.2.2.4)
|
||||
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.28.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.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.12.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.9.0)
|
||||
sentry-ruby (5.9.0)
|
||||
sentry-ruby (~> 5.12.0)
|
||||
sentry-ruby (5.12.0)
|
||||
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)
|
||||
@@ -366,53 +415,57 @@ 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-x86_64-linux)
|
||||
stimulus-rails (1.2.1)
|
||||
sqlite3 (1.6.7-arm64-darwin)
|
||||
sqlite3 (1.6.7-x86_64-linux)
|
||||
stimulus-rails (1.3.0)
|
||||
railties (>= 6.0.0)
|
||||
thor (1.2.2)
|
||||
tilt (2.2.0)
|
||||
timeout (0.3.2)
|
||||
turbo-rails (1.4.0)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.0)
|
||||
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)
|
||||
view_component (3.6.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)
|
||||
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
|
||||
aws-sdk-s3
|
||||
byebug (~> 11.1)
|
||||
capybara
|
||||
cssbundling-rails
|
||||
@@ -421,12 +474,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
|
||||
@@ -434,12 +489,14 @@ DEPENDENCIES
|
||||
listen (~> 3.2)
|
||||
lnurl
|
||||
lockbox
|
||||
manifique!
|
||||
net-ldap
|
||||
nostr!
|
||||
pagy (~> 6.0, >= 6.0.2)
|
||||
pg (~> 1.2.3)
|
||||
puma (~> 4.1)
|
||||
rails (~> 7.0.2)
|
||||
rails-controller-testing
|
||||
rails-settings-cached (~> 2.8.3)
|
||||
rqrcode (~> 2.0)
|
||||
rspec-rails
|
||||
|
||||
26
README.md
@@ -14,7 +14,6 @@ so:
|
||||
|
||||
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||
Docker Desktop)
|
||||
2. Uncomment the `redis`, `web`, and `sidekiq` sections in `docker-compose.yml`
|
||||
3. Run `docker compose up` and wait until 389ds announces its successful start
|
||||
in the log output
|
||||
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
||||
@@ -53,12 +52,14 @@ Running all specs:
|
||||
|
||||
### Docker (Compose)
|
||||
|
||||
There is a working Docker Compose config file, which allows you to spin up both
|
||||
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.
|
||||
|
||||
By default, `docker-compose up` will only start the LDAP server, listening on
|
||||
port 389 on your machine. Uncomment other services in `docker-compose.yml` if
|
||||
you want to use them.
|
||||
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
|
||||
listening on port 389 on your machine.
|
||||
|
||||
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
|
||||
the end of the docker compose command. eg. `docker compose up ldap redis`
|
||||
|
||||
#### LDAP server
|
||||
|
||||
@@ -78,6 +79,20 @@ The setup task will first delete any existing entries in the directory tree
|
||||
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
|
||||
with a fresh installation, delete both that directory as well as the container.
|
||||
|
||||
#### Minio / RS
|
||||
|
||||
If you want to run remoteStorage accounts locally, you will have to create the
|
||||
respective bucket first:
|
||||
|
||||
* `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
|
||||
|
||||
The following command downloads the specified npm module to `vendor/javascript`
|
||||
@@ -106,6 +121,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
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@import "components/animations";
|
||||
@import "components/base";
|
||||
@import "components/buttons";
|
||||
@import "components/dashboard_services";
|
||||
|
||||
16
app/assets/stylesheets/components/animations.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation-name: scaleIn;
|
||||
animation-duration: 0.15s;
|
||||
animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1);
|
||||
}
|
||||
@@ -14,12 +14,12 @@
|
||||
@apply py-1 px-2 text-sm;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply border-2 border-gray-100 hover:bg-gray-100;
|
||||
.btn-icon {
|
||||
@apply py-2 px-3;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@apply px-3;
|
||||
.btn-outline {
|
||||
@apply py-2 border-2 border-gray-100 hover:bg-gray-100;
|
||||
}
|
||||
|
||||
.btn-gray {
|
||||
|
||||
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
|
||||
26
app/components/dropdown_component.html.erb
Normal file
@@ -0,0 +1,26 @@
|
||||
<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">
|
||||
<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/kebab-menu", locals: {
|
||||
custom_class: "inline text-gray-500 h-6 w-6"
|
||||
} %>
|
||||
</span>
|
||||
</span>
|
||||
</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>
|
||||
5
app/components/dropdown_component.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DropdownComponent < ViewComponent::Base
|
||||
|
||||
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
|
||||
@@ -1,4 +1,6 @@
|
||||
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
|
||||
<%= tag.public_send(@tag, class: "mb-6 last:mb-0", data: {
|
||||
:'field-name' => @field_name
|
||||
}) do %>
|
||||
<% if @positioning == :vertical %>
|
||||
<label class="block">
|
||||
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
|
||||
@@ -9,7 +11,21 @@
|
||||
<%= @descripton %>
|
||||
</p>
|
||||
<% end %>
|
||||
<%= content %>
|
||||
|
||||
<%= tag.p class: "flex gap-x-1", data: {
|
||||
controller: @resettable ? "settings--resettable-field" : nil,
|
||||
} do %>
|
||||
<%= content %>
|
||||
<% if @resettable %>
|
||||
<button type="button"
|
||||
class="relative grow-0 shrink-0 btn-md btn-outline text-red-700"
|
||||
title="Reset to default value"
|
||||
data-settings--resettable-field-target="resetButton"
|
||||
data-action="settings--resettable-field#resetField">
|
||||
Reset
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</label>
|
||||
<% elsif @positioning == :horizontal %>
|
||||
<label class="block flex items-center justify-between">
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
module FormElements
|
||||
class FieldsetComponent < ViewComponent::Base
|
||||
def initialize(tag: "li", positioning: :vertical, title:, description: nil)
|
||||
def initialize(tag: "li", positioning: :vertical,
|
||||
title:, description: nil,
|
||||
field_name: nil, resettable: false)
|
||||
@tag = tag
|
||||
@positioning = positioning
|
||||
@title = title
|
||||
@descripton = description
|
||||
@field_name = field_name
|
||||
@resettable = resettable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
title: @title,
|
||||
description: @description,
|
||||
field_name: "setting_#{@key.to_s}",
|
||||
resettable: @resettable
|
||||
) do %>
|
||||
<%= method("#{@type}_field").call :setting, @key,
|
||||
value: Setting.public_send(@key),
|
||||
data: {
|
||||
:'default-value' => Setting.get_field(@key)[:default]
|
||||
},
|
||||
class: "w-full" %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module FormElements
|
||||
class FieldsetResettableSettingComponent < ViewComponent::Base
|
||||
def initialize(tag: "li", key:, type: :text, title:, description: nil)
|
||||
@tag = tag
|
||||
@positioning = :vertical
|
||||
@title = title
|
||||
@description = description
|
||||
@key = key.to_sym
|
||||
@type = type
|
||||
@resettable = is_resettable?(@key)
|
||||
end
|
||||
|
||||
def is_resettable?(key)
|
||||
default_value = Setting.get_field(key)[:default]
|
||||
default_value.present? && (default_value != Setting.send(key))
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/components/modal_component.html.erb
Normal file
@@ -0,0 +1,28 @@
|
||||
<div tabindex="-1" class="relative z-10">
|
||||
<!-- Modal Background -->
|
||||
<div class="hidden fixed inset-0 bg-black bg-opacity-80 overflow-y-auto flex items-center justify-center"
|
||||
data-modal-target="background"
|
||||
data-action="click->modal#closeBackground"
|
||||
data-transition-enter="transition-all ease-in-out duration-100"
|
||||
data-transition-enter-from="bg-opacity-0"
|
||||
data-transition-enter-to="bg-opacity-80"
|
||||
data-transition-leave="transition-all ease-in-out duration-100"
|
||||
data-transition-leave-from="bg-opacity-80"
|
||||
data-transition-leave-to="bg-opacity-0">
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div data-modal-target="container"
|
||||
class="max-h-screen w-auto max-w-lg relative
|
||||
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||
<!-- Modal Card -->
|
||||
<div class="m-1 bg-white rounded shadow">
|
||||
<div class="p-8">
|
||||
<%= content %>
|
||||
<div class="flex justify-end items-center flex-wrap mt-6">
|
||||
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
2
app/components/modal_component.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
class ModalComponent < ViewComponent::Base
|
||||
end
|
||||
6
app/components/qr_code_modal_component.html.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<%= render ModalComponent.new do %>
|
||||
<% if @descripton.present? %>
|
||||
<p class="mb-6"><%= @description %></p>
|
||||
<% end %>
|
||||
<p><%= raw @qr_code_svg %></p>
|
||||
<% end %>
|
||||
24
app/components/qr_code_modal_component.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
require "rqrcode"
|
||||
|
||||
class QrCodeModalComponent < ViewComponent::Base
|
||||
def initialize(qr_content:, description: nil)
|
||||
@description = description
|
||||
@qr_code_svg = qr_code_svg(qr_content)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def qr_code_svg(content)
|
||||
qr_code = RQRCode::QRCode.new(content)
|
||||
qr_code.as_svg(
|
||||
color: "000",
|
||||
shape_rendering: "crispEdges",
|
||||
module_size: 6,
|
||||
standalone: true,
|
||||
use_path: true,
|
||||
svg_attributes: {
|
||||
class: 'inline-block'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
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">
|
||||
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h4 class="mb-1 text-lg font-bold">
|
||||
<%= @web_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
|
||||
@@ -3,7 +3,7 @@ class Admin::Settings::ServicesController < Admin::SettingsController
|
||||
@service = params[:s]
|
||||
|
||||
if @service.blank?
|
||||
redirect_to admin_settings_services_path(params: { s: "discourse" })
|
||||
redirect_to admin_settings_services_path(params: { s: "btcpay" })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ class Admin::UsersController < Admin::BaseController
|
||||
end
|
||||
|
||||
@services_enabled = @user.services_enabled
|
||||
|
||||
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn, ou: @user.ou)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
29
app/controllers/api/btcpay_controller.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
class Api::BtcpayController < Api::BaseController
|
||||
before_action :require_feature_enabled
|
||||
|
||||
def onchain_btc_balance
|
||||
balance = BtcpayManager::FetchOnchainWalletBalance.call
|
||||
render json: balance
|
||||
rescue => error
|
||||
Rails.logger.warn "Failed to fetch BTC wallet balance: #{error.message}"
|
||||
render json: { error: 'Failed to fetch wallet balance' },
|
||||
status: 500
|
||||
end
|
||||
|
||||
def lightning_btc_balance
|
||||
balance = BtcpayManager::FetchLightningWalletBalance.call
|
||||
render json: balance
|
||||
rescue => error
|
||||
Rails.logger.warn "Failed to fetch BTC lightning balance: #{error.message}"
|
||||
render json: { error: 'Failed to fetch wallet balance' },
|
||||
status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_feature_enabled
|
||||
unless Setting.btcpay_publish_wallet_balances
|
||||
http_status :not_found and return
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
131
app/controllers/rs/oauth_controller.rb
Normal file
@@ -0,0 +1,131 @@
|
||||
class Rs::OauthController < ApplicationController
|
||||
before_action :require_signed_in_with_username, only: :new
|
||||
before_action :authenticate_user!, only: :create
|
||||
|
||||
def new
|
||||
@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]
|
||||
@state = params[:state]
|
||||
@root_access_requested = (@scopes & [":r",":rw"]).any?
|
||||
|
||||
@denial_url = url_with_state("#{@redirect_uri}#error=access_denied", @state)
|
||||
|
||||
@expire_at_dates = [["Never", nil],
|
||||
["In 1 month", 1.month.from_now],
|
||||
["In 1 day", 1.day.from_now]]
|
||||
|
||||
http_status :bad_request and return unless @redirect_uri.present?
|
||||
|
||||
unless current_user == @user
|
||||
sign_out :user
|
||||
|
||||
redirect_to new_rs_oauth_url(@user.cn,
|
||||
scope: params[:scope],
|
||||
redirect_uri: params[:redirect_uri],
|
||||
client_id: params[:client_id],
|
||||
state: params[:state])
|
||||
return
|
||||
end
|
||||
|
||||
unless @client_id.present?
|
||||
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_request", @state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
if @scopes.empty?
|
||||
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_scope", @state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
unless hostname_of(@client_id) == hostname_of(@redirect_uri)
|
||||
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_client", @state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
@client_id.gsub!(/http(s)?:\/\//, "")
|
||||
|
||||
if auth = current_user.remote_storage_authorizations.valid.where(permissions: @scopes, client_id: @client_id).first
|
||||
redirect_to(url_with_state("#{@redirect_uri}#access_token=#{auth.token}", @state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
unless current_user.id.to_s == params[:user_id]
|
||||
Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}")
|
||||
http_status :forbidden and return
|
||||
end
|
||||
|
||||
permissions = parse_scopes params[:scope]
|
||||
redirect_uri = params[:redirect_uri].presence
|
||||
client_id = params[:client_id].presence
|
||||
state = params[:state].presence
|
||||
expire_at = params[:expire_at].presence
|
||||
|
||||
http_status :bad_request and return unless redirect_uri.present?
|
||||
|
||||
if permissions.empty?
|
||||
redirect_to(url_with_state("#{redirect_uri}#error=invalid_scope", state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
unless client_id.present?
|
||||
redirect_to(url_with_state("#{redirect_uri}#error=invalid_request", state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
unless hostname_of(client_id) == hostname_of(redirect_uri)
|
||||
redirect_to(url_with_state("#{redirect_uri}#error=invalid_client", state),
|
||||
allow_other_host: true) and return
|
||||
end
|
||||
|
||||
client_id.gsub!(/http(s)?:\/\//, "")
|
||||
|
||||
auth = current_user.remote_storage_authorizations.create!(
|
||||
permissions: permissions,
|
||||
client_id: client_id,
|
||||
redirect_uri: redirect_uri,
|
||||
app_name: client_id,
|
||||
expire_at: expire_at
|
||||
)
|
||||
|
||||
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state),
|
||||
allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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)
|
||||
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
|
||||
end
|
||||
|
||||
def parse_scopes(scope_string)
|
||||
return [] if scope_string.blank?
|
||||
|
||||
scopes = scope_string.
|
||||
gsub(/\[|\]/, "").
|
||||
gsub(/\,/, " ").
|
||||
gsub(/\/:/, ":").
|
||||
split(/\s/).map(&:strip).
|
||||
reject(&:empty?)
|
||||
|
||||
scopes = [":r"] if scopes.include?("*:r")
|
||||
scopes = [":rw"] if scopes.include?("*:rw")
|
||||
|
||||
scopes
|
||||
end
|
||||
|
||||
def url_with_state(url, state)
|
||||
state ? "#{url}&state=#{CGI.escape(state)}" : url
|
||||
end
|
||||
|
||||
end
|
||||
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
|
||||
@@ -8,8 +8,7 @@ class Services::LightningController < ApplicationController
|
||||
before_action :fetch_balance
|
||||
|
||||
def index
|
||||
@wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||
initialize_lndhub_qr_code
|
||||
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||
end
|
||||
|
||||
def transactions
|
||||
@@ -56,20 +55,6 @@ class Services::LightningController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def initialize_lndhub_qr_code
|
||||
qr_code = RQRCode::QRCode.new(@wallet_url)
|
||||
@lndhub_qr_svg = qr_code.as_svg(
|
||||
color: "000",
|
||||
shape_rendering: "crispEdges",
|
||||
module_size: 6,
|
||||
standalone: true,
|
||||
use_path: true,
|
||||
svg_attributes: {
|
||||
class: 'inline-block'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def authenticate_with_lndhub(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
|
||||
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,13 +1,15 @@
|
||||
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_feature_enabled
|
||||
before_action :set_current_section
|
||||
before_action :require_service_available
|
||||
|
||||
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
|
||||
@@ -18,13 +20,7 @@ class Services::RemotestorageController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def require_service_enabled
|
||||
unless Setting.remotestorage_enabled?
|
||||
http_status :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
@current_section = :services
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.remotestorage_enabled?
|
||||
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
|
||||
@@ -19,10 +19,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(@user.dn, @user.display_name)
|
||||
end
|
||||
|
||||
if @user.avatar_new.present?
|
||||
LdapManager::UpdateAvatar.call(@user.dn, @user.avatar_new)
|
||||
end
|
||||
|
||||
redirect_to setting_path(@settings_section), flash: {
|
||||
@@ -105,7 +110,9 @@ class SettingsController < ApplicationController
|
||||
|
||||
def set_settings_section
|
||||
@settings_section = params[:section]
|
||||
allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments]
|
||||
allowed_sections = [
|
||||
:profile, :account, :lightning, :remotestorage, :xmpp, :experiments
|
||||
]
|
||||
|
||||
unless allowed_sections.include?(@settings_section.to_sym)
|
||||
redirect_to setting_path(:profile)
|
||||
@@ -117,8 +124,9 @@ 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}"
|
||||
|
||||
11
app/helpers/oauth_helper.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module OauthHelper
|
||||
|
||||
def scope_name(scope)
|
||||
scope.gsub(/(\:.+)/, '')
|
||||
end
|
||||
|
||||
def scope_permissions(scope)
|
||||
scope.match(/\:r$/) ? "r" : "rw"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Application } from "@hotwired/stimulus"
|
||||
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
|
||||
window.Stimulus = application
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "resetButton" ]
|
||||
|
||||
resetField () {
|
||||
const inputEl = this.element.querySelector('input')
|
||||
inputEl.value = inputEl.dataset.defaultValue
|
||||
}
|
||||
}
|
||||
10
app/jobs/remote_storage_expire_authorization_job.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class RemoteStorageExpireAuthorizationJob < ApplicationJob
|
||||
queue_as :remotestorage
|
||||
|
||||
def perform(rs_auth_id)
|
||||
rs_auth = RemoteStorageAuthorization.find rs_auth_id
|
||||
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
|
||||
|
||||
rs_auth.destroy!
|
||||
end
|
||||
end
|
||||
@@ -5,4 +5,16 @@ 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
|
||||
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
|
||||
|
||||
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(self)
|
||||
end
|
||||
end
|
||||
114
app/models/remote_storage_authorization.rb
Normal file
@@ -0,0 +1,114 @@
|
||||
class RemoteStorageAuthorization < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :web_app, class_name: "AppCatalog::WebApp", optional: true
|
||||
|
||||
serialize :permissions
|
||||
|
||||
validates_presence_of :permissions
|
||||
validates_presence_of :client_id
|
||||
|
||||
scope :valid, -> { where(expire_at: nil).or(where(expire_at: (DateTime.now)..)) }
|
||||
scope :expired, -> { where(expire_at: ..(DateTime.now)) }
|
||||
|
||||
after_initialize do |a|
|
||||
a.permissions = [] if a.permissions == nil
|
||||
end
|
||||
|
||||
before_create :generate_token
|
||||
before_create :store_token_in_redis
|
||||
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
|
||||
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
|
||||
path = start_url.gsub(/^\.\.\//, "").gsub(/^\.\//, "").gsub(/^\//, "")
|
||||
"#{url}/#{path}"
|
||||
end
|
||||
end
|
||||
|
||||
def delete_token_from_redis
|
||||
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
|
||||
|
||||
def redis
|
||||
@redis ||= Redis.new(url: Setting.rs_redis_url)
|
||||
end
|
||||
|
||||
def generate_token(length=16)
|
||||
self.token = SecureRandom.hex(length) if self.token.blank?
|
||||
end
|
||||
|
||||
def store_token_in_redis
|
||||
redis.sadd "authorizations:#{user.cn}:#{token}", permissions
|
||||
end
|
||||
|
||||
def schedule_token_expiry
|
||||
return unless expire_at.present?
|
||||
RemoteStorageExpireAuthorizationJob.set(wait_until: expire_at)
|
||||
.perform_later(id)
|
||||
end
|
||||
|
||||
def remove_token_expiry_job
|
||||
queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
|
||||
queue.each do |job|
|
||||
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
|
||||
job.delete if job.display_args == [id]
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
@@ -12,7 +12,7 @@ class Setting < RailsSettings::Base
|
||||
# Internal services
|
||||
#
|
||||
|
||||
field :redis_url, type: :string, readonly: true,
|
||||
field :redis_url, type: :string,
|
||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||
|
||||
#
|
||||
@@ -29,38 +29,67 @@ class Setting < RailsSettings::Base
|
||||
|
||||
field :xmpp_default_rooms, type: :array, default: []
|
||||
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
|
||||
field :xmpp_notifications_from_address, type: :string, default: primary_domain
|
||||
|
||||
#
|
||||
# Sentry
|
||||
#
|
||||
|
||||
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
|
||||
#
|
||||
|
||||
field :discourse_public_url, type: :string, readonly: true,
|
||||
field :discourse_public_url, type: :string,
|
||||
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, readonly: true,
|
||||
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, readonly: true,
|
||||
field :ejabberd_api_url, type: :string,
|
||||
default: ENV["EJABBERD_API_URL"].presence
|
||||
|
||||
field :ejabberd_admin_url, type: :string, readonly: true,
|
||||
field :ejabberd_admin_url, type: :string,
|
||||
default: ENV["EJABBERD_ADMIN_URL"].presence
|
||||
|
||||
field :ejabberd_buddy_roster, type: :string,
|
||||
@@ -70,50 +99,56 @@ class Setting < RailsSettings::Base
|
||||
# Gitea
|
||||
#
|
||||
|
||||
field :gitea_public_url, type: :string, readonly: true,
|
||||
field :gitea_public_url, type: :string,
|
||||
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
|
||||
#
|
||||
|
||||
field :lndhub_api_url, type: :string, readonly: true,
|
||||
field :lndhub_api_url, type: :string,
|
||||
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, readonly: true,
|
||||
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
|
||||
#
|
||||
|
||||
field :mastodon_public_url, type: :string, readonly: true,
|
||||
field :mastodon_public_url, type: :string,
|
||||
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
|
||||
#
|
||||
|
||||
field :mediawiki_public_url, type: :string, readonly: true,
|
||||
field :mediawiki_public_url, type: :string,
|
||||
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
|
||||
@@ -126,8 +161,11 @@ 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"
|
||||
end
|
||||
|
||||
@@ -2,10 +2,14 @@ class User < ApplicationRecord
|
||||
include EmailValidatable
|
||||
|
||||
attr_accessor :display_name
|
||||
attr_accessor :avatar_new
|
||||
|
||||
serialize :preferences, 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
|
||||
@@ -18,6 +22,12 @@ class User < ApplicationRecord
|
||||
|
||||
has_many :accounts, through: :lndhub_user
|
||||
|
||||
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/,
|
||||
@@ -38,10 +48,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,6 +90,7 @@ class User < ApplicationRecord
|
||||
# E-Mail update confirmed
|
||||
LdapManager::UpdateEmail.call(self.dn, self.email)
|
||||
else
|
||||
# TODO Make configurable
|
||||
# E-Mail from signup confirmed (i.e. account activation)
|
||||
enable_service %w[ discourse gitea mediawiki xmpp ]
|
||||
|
||||
@@ -107,6 +128,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?
|
||||
@@ -132,6 +158,10 @@ class User < ApplicationRecord
|
||||
@display_name ||= ldap_entry[:display_name]
|
||||
end
|
||||
|
||||
def avatar
|
||||
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn, ou: ou)
|
||||
end
|
||||
|
||||
def services_enabled
|
||||
ldap_entry[:service] || []
|
||||
end
|
||||
@@ -160,4 +190,17 @@ class User < ApplicationRecord
|
||||
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
|
||||
|
||||
52
app/services/app_catalog_manager/update_metadata.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@app.save!
|
||||
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}.png"
|
||||
key = "web_apps/#{@app.id}/icons/#{attachment_name}.png"
|
||||
|
||||
tempfile = Down.download(download_url)
|
||||
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
2
app/services/app_catalog_manager_service.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
class AppCatalogManagerService < ApplicationService
|
||||
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"
|
||||
|
||||
{
|
||||
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
|
||||
17
app/services/ldap_manager/fetch_avatar.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module LdapManager
|
||||
class FetchAvatar < LdapManagerService
|
||||
def initialize(cn:, ou: nil)
|
||||
@cn = cn
|
||||
@ou = ou
|
||||
end
|
||||
|
||||
def call
|
||||
treebase = @ou ? "ou=#{@ou},cn=users,#{suffix}" : ldap_config["base"]
|
||||
attributes = %w{ jpegPhoto }
|
||||
filter = Net::LDAP::Filter.eq("cn", @cn)
|
||||
|
||||
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
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,2 +1,5 @@
|
||||
class LdapManagerService < LdapService
|
||||
def suffix
|
||||
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
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)
|
||||
|
||||
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 %>
|
||||
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>
|
||||
@@ -8,16 +8,15 @@
|
||||
description: "Discourse configuration present and features enabled"
|
||||
) %>
|
||||
<% if Setting.discourse_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||
<%= f.text_field :discourse_public_url,
|
||||
value: Setting.discourse_public_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Connect secret") do %>
|
||||
<%= f.password_field :discourse_connect_secret,
|
||||
value: Setting.discourse_connect_secret,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :discourse_public_url,
|
||||
title: "Public URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :discourse_connect_secret,
|
||||
type: :password,
|
||||
title: "Connect secret"
|
||||
) %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% if Setting.discourse_enabled? %>
|
||||
@@ -31,14 +30,14 @@
|
||||
<input type="text" class="grow" disabled="disabled"
|
||||
value="https://<%= Setting.accounts_domain %>/discourse/connect"
|
||||
data-clipboard-target="source" />
|
||||
<button class="btn-md btn-icon btn-blue shrink-0"
|
||||
<button class="btn-md btn-icon btn-outline shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
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>
|
||||
@@ -8,16 +8,14 @@
|
||||
description: "ejabberd configuration present and features enabled"
|
||||
) %>
|
||||
<% if Setting.ejabberd_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
|
||||
<%= f.text_field :ejabberd_api_url,
|
||||
value: Setting.ejabberd_api_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Admin URL") do %>
|
||||
<%= f.text_field :ejabberd_admin_url,
|
||||
value: Setting.ejabberd_admin_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :ejabberd_api_url,
|
||||
title: "API URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :ejabberd_admin_url,
|
||||
title: "Admin URL"
|
||||
) %>
|
||||
</ul>
|
||||
<h3 class="mt-10">User default settings</h3>
|
||||
<ul role="list">
|
||||
@@ -37,12 +35,24 @@
|
||||
title: "Auto-join default rooms",
|
||||
description: "Automatically join above default rooms in chat clients"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :ejabberd_buddy_roster,
|
||||
title: "Contact roster name",
|
||||
description: "Used when exchanging contacts after signup from invitation"
|
||||
) %>
|
||||
</ul>
|
||||
<h3 class="mt-10">Notifications</h3>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
title: "From address",
|
||||
description: "Address (JID) of the account notifications are sent from",
|
||||
resettable: Setting.get_field(:xmpp_notifications_from_address)[:default] != Setting.xmpp_notifications_from_address
|
||||
) do %>
|
||||
<%= f.text_field :ejabberd_buddy_roster,
|
||||
value: Setting.ejabberd_buddy_roster,
|
||||
<%= f.text_field :xmpp_notifications_from_address,
|
||||
value: Setting.xmpp_notifications_from_address,
|
||||
data: {
|
||||
:'default-value' => Setting.get_field(:xmpp_notifications_from_address)[:default]
|
||||
},
|
||||
class: "w-full" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
description: "Gitea configuration present and features enabled"
|
||||
) %>
|
||||
<% if Setting.gitea_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||
<%= f.text_field :gitea_public_url,
|
||||
value: Setting.gitea_public_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :gitea_public_url,
|
||||
title: "Public URL"
|
||||
) %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -8,31 +8,36 @@
|
||||
description: "LNDHub configuration present and wallet features enabled"
|
||||
) %>
|
||||
<% if Setting.lndhub_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
|
||||
<%= f.text_field :lndhub_api_url,
|
||||
value: Setting.lndhub_api_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<% 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? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Public key", description: "The public key of the Lightning node used by LNDHub") do %>
|
||||
<%= f.text_field :lndhub_public_key,
|
||||
value: Setting.lndhub_public_key,
|
||||
class: "w-full", disabled: true %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :lndhub_api_url,
|
||||
title: "API URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
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>
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
description: "Mastodon configuration present and features enabled"
|
||||
) %>
|
||||
<% if Setting.mastodon_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||
<%= f.text_field :mastodon_public_url,
|
||||
value: Setting.mastodon_public_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :mastodon_public_url,
|
||||
title: "Public URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :mastodon_address_domain,
|
||||
title: "User address domain"
|
||||
) %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
description: "MediaWiki configuration present and features enabled"
|
||||
) %>
|
||||
<% if Setting.mediawiki_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||
<%= f.text_field :mediawiki_public_url,
|
||||
value: Setting.mediawiki_public_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :mediawiki_public_url,
|
||||
title: "Public URL"
|
||||
) %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<h3>RemoteStorage</h3>
|
||||
<p class="text-red-600 mb-8">Feature currently in development.</p>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
form: f,
|
||||
@@ -8,10 +9,13 @@
|
||||
description: "RemoteStorage configuration present and features enabled"
|
||||
) %>
|
||||
<% if Setting.remotestorage_enabled? %>
|
||||
<%= render FormElements::FieldsetComponent.new(title: "Storage URL") do %>
|
||||
<%= f.text_field :rs_storage_url,
|
||||
value: Setting.rs_storage_url,
|
||||
class: "w-full", disabled: true %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :rs_storage_url,
|
||||
title: "Storage Base URL"
|
||||
) %>
|
||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||
key: :rs_redis_url,
|
||||
title: "Redis URL"
|
||||
) %>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -63,6 +63,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>
|
||||
|
||||
@@ -7,73 +7,85 @@
|
||||
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.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.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.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 +96,19 @@
|
||||
<% 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.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 %>
|
||||
|
||||
@@ -12,7 +12,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 +21,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-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle <%= custom_class %>"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 445 B |
1
app/views/icons/_asterisk.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512" fill="currentColor" stroke="currentColor" stroke-width="2" class="<%= custom_class %>"><path d="M475.31 364.144L288 256l187.31-108.144c5.74-3.314 7.706-10.653 4.392-16.392l-4-6.928c-3.314-5.74-10.653-7.706-16.392-4.392L272 228.287V12c0-6.627-5.373-12-12-12h-8c-6.627 0-12 5.373-12 12v216.287L52.69 120.144c-5.74-3.314-13.079-1.347-16.392 4.392l-4 6.928c-3.314 5.74-1.347 13.079 4.392 16.392L224 256 36.69 364.144c-5.74 3.314-7.706 10.653-4.392 16.392l4 6.928c3.314 5.74 10.653 7.706 16.392 4.392L240 283.713V500c0 6.627 5.373 12 12 12h8c6.627 0 12-5.373 12-12V283.713l187.31 108.143c5.74 3.314 13.079 1.347 16.392-4.392l4-6.928c3.314-5.74 1.347-13.079-4.392-16.392z"/></svg>
|
||||
|
After Width: | Height: | Size: 760 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-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder <%= custom_class %>"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 331 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-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/_kebab-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 |
11
app/views/icons/_qr_code.html.erb
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg class="icon-qr-code <%= custom_class %>" fill="currentColor" width="90" height="90" version="1.1" viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="path2" d="m22.014 22.612c0-2.5389 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0587 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||
<path id="path4" d="m22.014 61.598c0-2.539 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0586 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||
<path id="path6" d="m50 22.612c0-2.5389 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0587 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||
<path id="path8" d="m50 61.598c0-2.539 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0586 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||
<path id="path10" d="m8.85 45c0-1.7397 1.4103-3.15 3.15-3.15h66.5c1.7397 0 3.15 1.4103 3.15 3.15s-1.4103 3.15-3.15 3.15h-66.5c-1.7397 0-3.15-1.4103-3.15-3.15z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||
<path id="path12" d="m11.566 0c-6.3876 0-11.566 5.1782-11.566 11.566v14.627c0 1.7713 1.4359 3.2073 3.2072 3.2073s3.2072-1.436 3.2072-3.2073v-14.627c0-2.845 2.3064-5.1514 5.1514-5.1514h14.627c1.7713 0 3.2073-1.4359 3.2073-3.2072s-1.436-3.2072-3.2073-3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||
<path id="path14" d="m11.566 90c-6.3876 0-11.566-5.1782-11.566-11.566v-14.628c0-1.7713 1.4359-3.2072 3.2072-3.2072s3.2072 1.4359 3.2072 3.2072v14.628c0 2.845 2.3064 5.1513 5.1514 5.1513h14.627c1.7713 0 3.2073 1.436 3.2073 3.2073 0 1.7712-1.436 3.2072-3.2073 3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||
<path id="path16" d="m78.434 0c6.3876 0 11.566 5.1782 11.566 11.566v14.627c0 1.7713-1.4359 3.2073-3.2072 3.2073s-3.2072-1.436-3.2072-3.2073v-14.627c0-2.845-2.3064-5.1514-5.1514-5.1514h-14.627c-1.7713 0-3.2073-1.4359-3.2073-3.2072s1.436-3.2072 3.2073-3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||
<path id="path18" d="m78.434 90c6.3876 0 11.566-5.1782 11.566-11.566v-14.628c0-1.7713-1.4359-3.2072-3.2072-3.2072s-3.2072 1.4359-3.2072 3.2072v14.628c0 2.845-2.3064 5.1513-5.1514 5.1513h-14.627c-1.7713 0-3.2073 1.436-3.2073 3.2073 0 1.7712 1.436 3.2072 3.2073 3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
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="<%= 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: 848 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 |
@@ -8,20 +8,27 @@
|
||||
</p>
|
||||
<ul class="md:w-3/4">
|
||||
<% @invitations_unused.each do |invitation| %>
|
||||
<li class="font-mono mb-2 flex gap-1" data-controller="clipboard">
|
||||
<input type="text" disabled class="relative grow"
|
||||
<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" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0 w-auto"
|
||||
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||
data-action="click->modal#open" title="Show QR code">
|
||||
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</button>
|
||||
<%= render QrCodeModalComponent.new(qr_content: invitation_url(invitation.token)) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
Hi <%= @user.display_name.presence || @user.cn %>,
|
||||
|
||||
You have just granted '<%= @auth.client_id %>' access to your Kosmos Storage, with the following permissions:
|
||||
|
||||
<% @permissions.each do |p| %>
|
||||
* <%= p %>
|
||||
<% end %>
|
||||
|
||||
Visit your Storage dashboard to check on your connected apps and revoke permissions anytime:
|
||||
|
||||
<%= services_storage_url %>
|
||||
|
||||
Have fun!
|
||||
|
||||
---
|
||||
|
||||
You can disable email notifications for new app authorizations in your account settings:
|
||||
<%= setting_url(:remotestorage) %>
|
||||
<% if Setting.discourse_enabled %>
|
||||
|
||||
If you have any questions, please visit our community forums:
|
||||
<%= Setting.discourse_public_url %>
|
||||
<% end %>
|
||||
58
app/views/rs/oauth/new.html.erb
Normal file
@@ -0,0 +1,58 @@
|
||||
<%= render HeaderCompactComponent.new(title: "Storage") %>
|
||||
|
||||
<%= render MainCompactComponent.new do %>
|
||||
<section class="permissions">
|
||||
<p class="mb-8">
|
||||
The app on
|
||||
<%= link_to @client_id, "https://#{@client_id}", class: "ks-text-link" %>
|
||||
is asking for access to these folders:
|
||||
</p>
|
||||
|
||||
<% if @root_access_requested %>
|
||||
<p class="scope text-lg">
|
||||
<span class="text-red-700">
|
||||
<%= render partial: "icons/alert-triangle",
|
||||
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
|
||||
All files and directories
|
||||
</span>
|
||||
<% if (@scopes & [":r"]).any? %>
|
||||
<span class="text-sm text-gray-500">(read only)</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% else %>
|
||||
<% @scopes.each do |scope| %>
|
||||
<p class="scope text-gray-600">
|
||||
<span class="text-lg">
|
||||
<%= render partial: "icons/folder",
|
||||
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
|
||||
<%= scope_name(scope) %>
|
||||
</span>
|
||||
<% if scope_permissions(scope) == "r" %>
|
||||
<span>(read only)</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= form_with(url: rs_oauth_path, method: :post, data: { turbo: false }) do |f| %>
|
||||
<%= f.hidden_field :redirect_uri, value: @redirect_uri %>
|
||||
<%= f.hidden_field :scope, value: @scopes.join(" ") %>
|
||||
<%= f.hidden_field :user_id, value: @user.id %>
|
||||
<%= f.hidden_field :client_id, value: @client_id %>
|
||||
<%= f.hidden_field :state, value: @state %>
|
||||
<p class="mt-8 mb-6">
|
||||
<%= f.label :expire_at, "Permission expires:", class: "mr-1.5" %>
|
||||
<%= f.select :expire_at, options_for_select(@expire_at_dates) %>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
You can revoke access for this app at any time on your storage dashboard.
|
||||
</p>
|
||||
<p class="mt-8 flex flex-col sm:flex-row gap-3 sm:gap-2 sm:justify-items-stretch">
|
||||
<%= f.submit "Allow",
|
||||
class: "btn-md btn-blue w-full sm:order-last sm:grow",
|
||||
data: { disable_with: "Saving..." } %>
|
||||
<%= link_to "Deny", @denial_url, class: "btn-md btn-gray text-red-700 w-full sm:grow" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
199
app/views/services/chat/show.html.erb
Normal file
@@ -0,0 +1,199 @@
|
||||
<%= render HeaderComponent.new(title: "Chat") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<p class="mb-6">
|
||||
Chat with anyone on the open Jabber (XMPP) network. Message people directly, or
|
||||
join public channels or private rooms.
|
||||
</p>
|
||||
</section>
|
||||
<section data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<h3>Your Chat Address</h3>
|
||||
<p class="mb-6">
|
||||
When you exchange contacts with people, give them your
|
||||
address, or add them using their address:
|
||||
</p>
|
||||
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
|
||||
<input type="text" id="user_address" class="grow"
|
||||
value=<%= current_user.address %> disabled="disabled"
|
||||
data-clipboard-target="source" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||
data-action="click->modal#open" title="Show QR code">
|
||||
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</button>
|
||||
</p>
|
||||
<%= render QrCodeModalComponent.new(qr_content: "xmpp:"+current_user.address) %>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Chat Apps</h3>
|
||||
<p>
|
||||
Use your account with many different apps, and on any devices you wish!
|
||||
When opening an app for the first time, just enter your address and
|
||||
password to log in.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Recommended Apps</h3>
|
||||
<div data-controller="tabs"
|
||||
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
|
||||
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
|
||||
class="mb-12">
|
||||
<select data-action="tabs#change" data-tabs-target="select"
|
||||
class="block w-full mb-8 sm:hidden">
|
||||
<optgroup label="Mobile">
|
||||
<option>Android</option>
|
||||
<option>iOS</option>
|
||||
</optgroup>
|
||||
<optgroup label="Desktop">
|
||||
<option>Linux</option>
|
||||
<option>Windows</option>
|
||||
<option>macOS</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Android
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
iOS
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Linux
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Windows
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
macOS
|
||||
</a>
|
||||
</li>
|
||||
<!-- <li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change"> -->
|
||||
<!-- <a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline"> -->
|
||||
<!-- Web -->
|
||||
<!-- </a> -->
|
||||
<!-- </li> -->
|
||||
</ul>
|
||||
|
||||
<div id="apps-android" class="hidden grid grid-cols-1 gap-6"
|
||||
data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Conversations",
|
||||
description: "The gold standard for Jabber on mobile devices",
|
||||
icon_path: "/img/logos/icon_conversations.png",
|
||||
links: [
|
||||
["Website", "https://conversations.im"],
|
||||
["Google Play", "https://play.google.com/store/apps/details?id=eu.siacs.conversations"],
|
||||
["F-Droid", "https://f-droid.org/en/packages/eu.siacs.conversations/"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div id="apps-ios" class="hidden grid grid-cols-1 gap-6"
|
||||
data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Siskin IM",
|
||||
description: "Lightweight and powerful chat app for iPhone and iPad",
|
||||
icon_path: "/img/logos/logo_siskin.png",
|
||||
links: [
|
||||
["Website", "https://siskin.im"],
|
||||
["App Store", "https://apps.apple.com/us/app/tigase-messenger/id1153516838"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Monal",
|
||||
description: "A chat app for iOS, iPadOS, and macOS",
|
||||
icon_path: "/img/logos/icon_monal.svg",
|
||||
icon_fill_box: true,
|
||||
links: [
|
||||
["Website", "https://monal-im.org"],
|
||||
["App Store", "https://apps.apple.com/app/id317711500"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div id="apps-linux" class="hidden grid grid-cols-1 gap-6"
|
||||
data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Dino",
|
||||
description: "A modern and simple chat app for Linux (good for GNOME)",
|
||||
icon_path: "/img/logos/icon_dino.svg",
|
||||
links: [
|
||||
["Website", "https://dino.im"],
|
||||
["Install from package", "https://github.com/dino/dino/wiki/Distribution-Packages"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Kaidan",
|
||||
description: "A fairly new, user-friendly chat app for all devices (good for KDE)",
|
||||
icon_path: "/img/logos/icon_kaidan.svg",
|
||||
links: [
|
||||
["Website", "https://kaidan.im"],
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Gajim",
|
||||
description: "A fully-featured chat app for Linux and Windows",
|
||||
icon_path: "/img/logos/icon_gajim.png",
|
||||
links: [
|
||||
["Website", "https://gajim.org/"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
|
||||
data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Gajim",
|
||||
description: "A fully-featured chat app for Linux and Windows",
|
||||
icon_path: "/img/logos/icon_gajim.png",
|
||||
links: [
|
||||
["Website", "https://gajim.org/"],
|
||||
["Microsoft Store", "https://apps.microsoft.com/store/detail/9PGGF6HD43F9?launch=true&mode=mini"],
|
||||
["Download options", "https://gajim.org/download/"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div id="apps-mac" class="hidden grid grid-cols-1 gap-6"
|
||||
data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Beagle IM",
|
||||
description: "Lightweight and powerful chat app for macOS",
|
||||
icon_path: "/img/logos/logo_beagle.png",
|
||||
links: [
|
||||
["Website", "https://beagle.im"],
|
||||
["App Store", "https://apps.apple.com/us/app/beagleim-by-tigase-inc/id1445349494"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Monal",
|
||||
description: "A chat app for iOS, iPadOS, and macOS",
|
||||
icon_path: "/img/logos/icon_monal.svg",
|
||||
icon_fill_box: true,
|
||||
links: [
|
||||
["Website", "https://monal-im.org"],
|
||||
["App Store", "https://apps.apple.com/app/id1637078500"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<!-- <div class="hidden grid grid-cols-1 gap-4 sm:gap-6" data-tabs-target="panel"> -->
|
||||
<!-- Web -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
@@ -16,20 +16,20 @@
|
||||
<input type="text" id="user_address" class="grow"
|
||||
value=<%= current_user.address %> disabled="disabled"
|
||||
data-clipboard-target="source" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<h3>Wallet Apps</h3>
|
||||
<p>
|
||||
You can connect various wallet apps to your Kosmos account. This allows
|
||||
@@ -40,19 +40,16 @@
|
||||
</p>
|
||||
<p data-controller="clipboard" class="my-6 text-center md:text-left">
|
||||
<input type="text" disabled class="hidden" aria-hidden=true
|
||||
value="<%= @wallet_url%>" data-clipboard-target="source" />
|
||||
value="<%= @wallet_setup_url %>" data-clipboard-target="source" />
|
||||
<button id="copy-setup-code" class="btn-md btn-blue w-full sm:w-auto"
|
||||
data-action="clipboard#copy" data-clipboard-target="trigger">
|
||||
<span class="content-initial">Copy setup code/URL</span>
|
||||
<span class="content-active hidden">Copied ✔</span>
|
||||
</button>
|
||||
<span class="mx-2 my-2 md:my-0 block md:inline">or</span>
|
||||
<button id="show-setup-code" class="btn-md btn-blue w-full sm:w-auto">Show setup QR code</button>
|
||||
<button id="hide-setup-code" class="hidden btn-md btn-blue w-full sm:w-auto">Hide setup QR code</button>
|
||||
</p>
|
||||
<p id="setup-code" class="hidden my-10 w-full text-center">
|
||||
<%= raw @lndhub_qr_svg %>
|
||||
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">Show setup QR code</button>
|
||||
</p>
|
||||
<%= render QrCodeModalComponent.new(qr_content: @wallet_setup_url) %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -119,27 +116,3 @@
|
||||
</p>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
const buttonShow = document.querySelector('#show-setup-code');
|
||||
const buttonHide = document.querySelector('#hide-setup-code');
|
||||
const setupCode = document.querySelector('#setup-code');
|
||||
|
||||
buttonShow.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
setupCode.classList.remove('hidden');
|
||||
buttonHide.classList.remove('hidden');
|
||||
buttonShow.classList.add('hidden');
|
||||
setupCode.scrollIntoView({behavior: "smooth", block: "nearest"});
|
||||
});
|
||||
|
||||
buttonHide.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
const el = document.querySelector('#setup-code');
|
||||
setupCode.classList.add('hidden');
|
||||
buttonHide.classList.add('hidden');
|
||||
buttonShow.classList.remove('hidden');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
219
app/views/services/mastodon/show.html.erb
Normal file
@@ -0,0 +1,219 @@
|
||||
<%= render HeaderComponent.new(title: "Social") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<p class="mb-6">
|
||||
Follow and interact with anyone on the open social web, from your Kosmos Mastodon account.
|
||||
</p>
|
||||
</section>
|
||||
<section data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<h3>Your User Address</h3>
|
||||
<p class="mb-6">
|
||||
Others can follow you under this address:
|
||||
</p>
|
||||
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
|
||||
<input type="text" id="user_address" class="grow"
|
||||
value=<%= current_user.mastodon_address %> disabled="disabled"
|
||||
data-clipboard-target="source" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||
data-action="click->modal#open" title="Show QR code">
|
||||
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</button>
|
||||
</p>
|
||||
<%= render QrCodeModalComponent.new(qr_content: current_user.address) %>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Social Apps</h3>
|
||||
<p>
|
||||
Use your Mastodon account with many different apps, and on any devices
|
||||
you wish! When adding your account to an app, you will log in via
|
||||
<a href="https://kosmos.social" target="_blank" class="ks-text-link">kosmos.social</a>.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Recommended Apps</h3>
|
||||
<div data-controller="tabs"
|
||||
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
|
||||
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
|
||||
class="mb-12">
|
||||
<select data-action="tabs#change" data-tabs-target="select"
|
||||
class="block w-full mb-8 sm:hidden">
|
||||
<option>Web</option>
|
||||
<optgroup label="Mobile">
|
||||
<option>Android</option>
|
||||
<option>iOS</option>
|
||||
</optgroup>
|
||||
<optgroup label="Desktop">
|
||||
<option>Linux</option>
|
||||
<option>Windows</option>
|
||||
<option>macOS</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Web
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-5 font-semibold no-underline">
|
||||
Android
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
iOS
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Linux
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
Windows
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||
macOS
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "kosmos.social",
|
||||
description: "The official Web app",
|
||||
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||
links: [
|
||||
["Launch", "https://kosmos.social"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Elk",
|
||||
description: " A nimble Mastodon web client",
|
||||
icon_path: "/img/logos/icon_elk.svg",
|
||||
links: [
|
||||
["Launch", "https://elk.zone"],
|
||||
["GitHub", "https://github.com/elk-zone/elk"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Sengi",
|
||||
description: "A cross-platform app, inspired by TweetDeck",
|
||||
icon_path: "/img/logos/icon_sengi.png",
|
||||
links: [
|
||||
["Website", "https://nicolasconstant.github.io/sengi/"],
|
||||
["GitHub", "https://github.com/NicolasConstant/sengi"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Mastodon for Android",
|
||||
description: "Android client by the Mastodon core team",
|
||||
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||
links: [
|
||||
["Website", "https://joinmastodon.org/apps"],
|
||||
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Fedilab",
|
||||
description: "Android client with many features",
|
||||
icon_path: "/img/logos/icon_fedilab.png",
|
||||
links: [
|
||||
["Website", "https://fedilab.app"],
|
||||
["Google Play", "https://play.google.com/store/apps/details?id=app.fedilab.android"],
|
||||
["F-Droid", "https://f-droid.org/packages/fr.gouv.etalab.mastodon"],
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Megalodon",
|
||||
description: "A popular fork of the official Android app",
|
||||
icon_path: "/img/logos/icon_megalodon.png",
|
||||
icon_fill_box: true,
|
||||
links: [
|
||||
["Website", "https://sk22.github.io/megalodon/"],
|
||||
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Mastodon for iOS",
|
||||
description: "iOS client by the Mastodon core team",
|
||||
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||
links: [
|
||||
["Website", "https://joinmastodon.org/apps"],
|
||||
["App Store", "https://apps.apple.com/us/app/mastodon-for-iphone/id1571998974"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Ice Cubes",
|
||||
description: "Slick, fast, open source, and with customizable UI",
|
||||
icon_path: "/img/logos/icon_icecubes.png",
|
||||
icon_fill_box: true,
|
||||
links: [
|
||||
["App Store", "https://apps.apple.com/us/app/ice-cubes-for-mastodon/id6444915884"],
|
||||
["GitHub", "https://github.com/Dimillian/IceCubesApp"]
|
||||
]
|
||||
) %>
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Mammoth",
|
||||
description: " Powerful, fast, feature-rich",
|
||||
icon_path: "/img/logos/icon_mammoth.png",
|
||||
links: [
|
||||
["Website", "https://getmammoth.app/"],
|
||||
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Tuba",
|
||||
description: "A simple, fast Mastodon app for Linux (good on GNOME)",
|
||||
icon_path: "/img/logos/icon_tuba.svg",
|
||||
links: [
|
||||
["Website", "https://tuba.geopjr.dev"],
|
||||
["Flathub", "https://flathub.org/apps/dev.geopjr.Tuba"],
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Sengi",
|
||||
description: "A cross-platform app, inspired by TweetDeck",
|
||||
icon_path: "/img/logos/icon_sengi.png",
|
||||
links: [
|
||||
["Website", "https://nicolasconstant.github.io/sengi/"],
|
||||
["GitHub", "https://github.com/NicolasConstant/sengi"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||
<%= render AppInfoComponent.new(
|
||||
name: "Mastonaut",
|
||||
description: "Simple, elegant, and native Mastodon client for Mac",
|
||||
icon_path: "/img/logos/icon_mastonaut.png",
|
||||
links: [
|
||||
["Launch", "https://www.mastonaut.app"],
|
||||
["Mac App Store", "https://apps.apple.com/app/mastonaut/id1450757574"]
|
||||
]
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
@@ -1,7 +0,0 @@
|
||||
<%= render HeaderComponent.new(title: "Storage") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<h3>Feature enabled</h3>
|
||||
</section>
|
||||
<% end %>
|
||||
16
app/views/services/remotestorage/show.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<%= render HeaderComponent.new(title: "Storage") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
<h3 class="mb-10">Connected Apps</h3>
|
||||
<% if @rs_auths.any? %>
|
||||
<div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-y-10 gap-x-12">
|
||||
<% @rs_auths.each do |auth| %>
|
||||
<%= render RsAuthComponent.new(auth: auth) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p>No apps connected yet.</p>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
@@ -13,13 +13,13 @@
|
||||
'settings--account--email-target': 'emailField'
|
||||
}, required: true %>
|
||||
<button type="button" id="edit-email"
|
||||
class="btn-md btn-icon btn-blue shrink-0 hidden initial-visible"
|
||||
class="btn-md btn-icon btn-outline shrink-0 hidden initial-visible"
|
||||
data-settings--account--email-target="editEmailButton"
|
||||
data-action="settings--account--email#editEmail"
|
||||
title="Edit email address">
|
||||
<span class="">
|
||||
<%= render partial: "icons/edit-3", locals: {
|
||||
custom_class: "text-white h-4 w-4 inline" } %>
|
||||
custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -1,33 +1,62 @@
|
||||
<section>
|
||||
<h3>Profile</h3>
|
||||
<p class="mb-2">
|
||||
<%= label :user_address, 'User address', class: 'font-bold' %>
|
||||
</p>
|
||||
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
|
||||
<input type="text" id="user_address" class="grow"
|
||||
value=<%= @user.address %> disabled="disabled"
|
||||
data-clipboard-target="source" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Your user address for Chat and Lightning Network.
|
||||
</p>
|
||||
<div class="mb-6">
|
||||
<p class="mb-2">
|
||||
<%= label :user_address, 'User address', class: 'font-bold' %>
|
||||
</p>
|
||||
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
|
||||
<input type="text" id="user_address" class="grow"
|
||||
value=<%= @user.address %> disabled="disabled"
|
||||
data-clipboard-target="source" />
|
||||
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||
title="Copy to clipboard">
|
||||
<span class="content-initial">
|
||||
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
<span class="content-active hidden">
|
||||
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Your user address for Chat and Lightning Network.
|
||||
</p>
|
||||
</div>
|
||||
<%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %>
|
||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
|
||||
<%= f.text_field :display_name, class: "w-full sm:w-3/5 mb-2" %>
|
||||
<%= f.text_field :display_name, class: "w-full sm:w-3/5" %>
|
||||
<% if @validation_errors.present? && @validation_errors[:display_name].present? %>
|
||||
<p class="error-msg"><%= @validation_errors[:display_name].first %></p>
|
||||
<p class="error-msg mt-2"><%= @validation_errors[:display_name].first %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<label class="block">
|
||||
<p class="font-bold mb-1">
|
||||
Avatar
|
||||
</p>
|
||||
<p class="text-gray-500">
|
||||
Default profile picture
|
||||
</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<% if current_user.avatar.present? %>
|
||||
<p class="flex-none">
|
||||
<%= image_tag "data:image/jpeg;base64,#{current_user.avatar}", class: "h-24 w-24 rounded-lg" %>
|
||||
</p>
|
||||
<% end %>
|
||||
<div class="grow">
|
||||
<p class="mb-2">
|
||||
<%= f.file_field :avatar, class: "" %>
|
||||
<p class="text-sm text-gray-500">
|
||||
JPEG or PNG image, not larger than 1 megabyte
|
||||
</p>
|
||||
<% if @validation_errors.present? && @validation_errors[:avatar].present? %>
|
||||
<p class="error-msg mb-2"><%= @validation_errors[:avatar].first %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
|
||||
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||
</p>
|
||||
|
||||
25
app/views/settings/_remotestorage.html.erb
Normal file
@@ -0,0 +1,25 @@
|
||||
<%= form_for @user, url: setting_path(:remotestorage), html: { :method => :put } do |f| %>
|
||||
<section>
|
||||
<h3>Notifications</h3>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
positioning: :horizontal,
|
||||
title: "New connection authorized",
|
||||
description: "Notify me when my storage is connected to a new app"
|
||||
) do %>
|
||||
<% f.fields_for :preferences do |p| %>
|
||||
<%= p.select :remotestorage_notify_auth_created, options_for_select([
|
||||
["off", "disabled"],
|
||||
["Chat (Jabber)", "xmpp"], # TODO make DRY, check for XMPP enabled
|
||||
["E-Mail", "email"]
|
||||
], selected: @user.preferences[:remotestorage_notify_auth_created]) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<p class="pt-6 border-t border-gray-200 text-right">
|
||||
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||
</p>
|
||||
</section>
|
||||
<% end %>
|
||||