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