Compare commits
129 Commits
99dc36f13a
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f08bb56a7a
|
||
| c1f275463e | |||
| 324809f77e | |||
|
|
f9b07bcb01
|
||
|
|
986eb5387c
|
||
| f76e2c2f14 | |||
|
|
22a7bbe6eb
|
||
| 18f4deb30f | |||
|
|
9f9bf6fd80
|
||
|
|
d2987da70a
|
||
|
|
6b7a80e23a
|
||
|
|
42b9b27561
|
||
|
|
c17c980b69
|
||
|
|
f199d5d12a
|
||
|
|
4b17afa93d
|
||
|
|
6d52af53ae
|
||
|
|
4c5ad67652
|
||
|
|
3437a756eb
|
||
| 0d9fc4aa74 | |||
| 82475161a9 | |||
|
|
fb3b9af3e5
|
||
|
|
b1a0268e6b
|
||
| e1e7d8f87d | |||
|
|
5b46f3adf5
|
||
|
|
a8a8fba14c
|
||
|
|
8a7016a30b
|
||
|
|
e2618de7c6
|
||
| 90680368fb | |||
|
|
8d90847896 | ||
|
|
8da297811b | ||
|
|
fa56d6b772 | ||
|
|
ca1221e9f3 | ||
|
|
295d486761 | ||
|
|
e00390d102 | ||
|
|
b947480190 | ||
|
|
fa07978aac | ||
|
|
e758e258a8 | ||
|
|
805733939c | ||
|
|
f050d010fd
|
||
|
|
95fac38b53
|
||
| cb80465297 | |||
|
|
c7550b4f64
|
||
| 341284aa99 | |||
|
|
b34d040ce3
|
||
| 1142a4e2d5 | |||
|
|
f2c7aa2f09
|
||
| cca44d7542 | |||
| cdad7546fb | |||
| feb7833533 | |||
|
|
dfb12b8f62
|
||
|
|
6c2a97e7e5
|
||
| c8b65de7f6 | |||
| 2861254adf | |||
| 1d2910dadb | |||
|
|
251a170f2b
|
||
|
|
cbbb4c6e47
|
||
|
|
3aad27c7bd
|
||
|
|
7cff849d79
|
||
|
|
75ffd4e2f1
|
||
| b84f9109f6 | |||
| 7fd564726f | |||
|
b2a1b8caf5
|
|||
|
52cc2a8151
|
|||
|
|
c8e405d93a
|
||
|
|
5f74212603
|
||
|
|
1c3e893b6b
|
||
|
|
eec4533fea
|
||
|
|
6d20ac9a1c
|
||
|
|
27dd4163f0
|
||
|
|
1a55e5e895
|
||
|
|
8eb487600c
|
||
|
|
678e80a25d
|
||
|
|
30fb9805e5
|
||
|
|
e675970f4c
|
||
|
|
a0727e709f
|
||
|
|
55abbcc5ad
|
||
|
|
ffed398024
|
||
|
|
1a2482434c
|
||
| b530ad2f0f | |||
|
|
3c2fe7c15d
|
||
| aa7044dea7 | |||
|
|
a3f0d0f2cf
|
||
|
|
dc63506102
|
||
|
|
b87b9c2437
|
||
|
|
e580cc9991
|
||
|
|
68ab88c481
|
||
|
|
c7fe1bc3bc
|
||
| 84337c3a7d | |||
| 654b90f9ee | |||
| aa0ba18763 | |||
|
|
7dae66959e
|
||
|
|
b67d6139ac
|
||
|
|
b9259958f4
|
||
|
|
832d1e3bd7
|
||
|
|
f3f967f9f7
|
||
|
|
9407c7a94d
|
||
|
|
df3ec9f90a
|
||
|
|
25a0723166
|
||
|
|
6e884b789a
|
||
|
|
346e36e160
|
||
|
|
b7bf957dd2
|
||
|
|
084835f06a
|
||
|
|
cd7b05e2ff
|
||
|
|
7280a4c023
|
||
|
|
164400adec
|
||
|
|
c2e0909132
|
||
|
|
c44ce61e25
|
||
|
|
e2294c4029
|
||
|
|
bdc03a7181
|
||
|
|
959449a3f4
|
||
|
|
b4c9b31ce7
|
||
|
|
43f133ebd7
|
||
|
|
d9e767298b
|
||
|
|
dd482d7f2e
|
||
|
|
09d99ce9c2
|
||
|
|
8f9e1c3e84
|
||
| 4a045bf61c | |||
| f62e49f524 | |||
|
|
b0c787bbc7
|
||
|
|
86dc44d096
|
||
|
|
a1663b9f9d
|
||
|
|
aa3c2b4fa2
|
||
|
|
4c0d8283e3
|
||
|
|
d4a3f8dadb
|
||
|
|
9e988e92d1
|
||
|
|
4232df302b
|
||
|
|
2c8b3cdacc
|
||
|
|
51952ecdc2
|
||
|
|
68e0d00f6e
|
@@ -28,7 +28,7 @@ steps:
|
|||||||
- bundle install --jobs=3 --retry=3
|
- bundle install --jobs=3 --retry=3
|
||||||
- yarn install
|
- yarn install
|
||||||
- rake css:build
|
- rake css:build
|
||||||
- rake spec
|
- bundle exec rspec
|
||||||
- name: rebuild-cache
|
- name: rebuild-cache
|
||||||
image: drillster/drone-volume-cache
|
image: drillster/drone-volume-cache
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
28
.env.example
28
.env.example
@@ -1,11 +1,37 @@
|
|||||||
|
SMTP_SERVER=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_LOGIN=accounts
|
||||||
|
SMTP_PASSWORD=123abc
|
||||||
|
SMTP_FROM_ADDRESS=accounts@example.com
|
||||||
|
SMTP_DOMAIN=example.com
|
||||||
|
SMTP_AUTH_METHOD=plain
|
||||||
|
SMTP_ENABLE_STARTTLS=auto
|
||||||
|
|
||||||
|
REDIS_URL='redis://localhost:6379/1'
|
||||||
|
|
||||||
LDAP_HOST=localhost
|
LDAP_HOST=localhost
|
||||||
LDAP_PORT=389
|
LDAP_PORT=389
|
||||||
LDAP_ADMIN_PASSWORD=passthebutter
|
LDAP_ADMIN_PASSWORD=passthebutter
|
||||||
LDAP_SUFFIX="dc=kosmos,dc=org"
|
LDAP_SUFFIX='dc=kosmos,dc=org'
|
||||||
|
|
||||||
|
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||||
|
|
||||||
|
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||||
|
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||||
|
MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||||
|
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||||
|
|
||||||
|
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||||
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||||
|
|
||||||
BTCPAY_API_URL='http://localhost:23001/api/v1'
|
BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||||
|
|
||||||
LNDHUB_API_URL='http://localhost:3023'
|
LNDHUB_API_URL='http://localhost:3023'
|
||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
|
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||||
|
LNDHUB_ADMIN_UI=true
|
||||||
|
LNDHUB_PG_HOST=localhost
|
||||||
|
LNDHUB_PG_PORT=5432
|
||||||
|
LNDHUB_PG_DATABASE=lndhub
|
||||||
|
LNDHUB_PG_USERNAME=lndhub
|
||||||
|
LNDHUB_PG_PASSWORD=''
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
EJABBERD_API_URL='https://xmpp.kosmos.org:5443/api'
|
|
||||||
|
|
||||||
BTCPAY_API_URL='http://10.1.1.163:23001/api/v1'
|
|
||||||
|
|
||||||
LNDHUB_LEGACY_API_URL='http://10.1.1.163:3026'
|
|
||||||
LNDHUB_API_URL='http://10.1.1.163:3026'
|
|
||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
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'
|
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||||
LNDHUB_LEGACY_API_URL='http://localhost:3023'
|
|
||||||
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'
|
||||||
|
|
||||||
|
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
|
||||||
|
|||||||
13
.gitea/release-drafter.yml
Normal file
13
.gitea/release-drafter.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name-template: 'v$RESOLVED_VERSION'
|
||||||
|
tag-template: 'v$RESOLVED_VERSION'
|
||||||
|
version-resolver:
|
||||||
|
major:
|
||||||
|
labels:
|
||||||
|
- 'release/major'
|
||||||
|
minor:
|
||||||
|
labels:
|
||||||
|
- 'release/minor'
|
||||||
|
patch:
|
||||||
|
labels:
|
||||||
|
- 'release/patch'
|
||||||
|
default: patch
|
||||||
11
.gitea/workflows/release_drafter.yml
Normal file
11
.gitea/workflows/release_drafter.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
jobs:
|
||||||
|
release_drafter_job:
|
||||||
|
name: Update release notes draft
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Release Drafter
|
||||||
|
uses: https://github.com/raucao/gitea-release-drafter@dev
|
||||||
15
Dockerfile
15
Dockerfile
@@ -1,8 +1,13 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM ruby:2.7.6
|
FROM ruby:2.7.6
|
||||||
RUN apt-get update -qq && apt-get install -y curl ldap-utils
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
|
||||||
|
ldap-utils tini
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||||
RUN apt-get update && apt-get install -y nodejs
|
RUN apt-get update && apt-get install -y nodejs
|
||||||
|
|
||||||
WORKDIR /akkounts
|
WORKDIR /akkounts
|
||||||
COPY Gemfile /akkounts/Gemfile
|
COPY Gemfile /akkounts/Gemfile
|
||||||
COPY Gemfile.lock /akkounts/Gemfile.lock
|
COPY Gemfile.lock /akkounts/Gemfile.lock
|
||||||
@@ -12,11 +17,5 @@ RUN gem install foreman
|
|||||||
RUN npm install -g yarn
|
RUN npm install -g yarn
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
# Add a script to be executed every time the container starts.
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
COPY docker/entrypoint.sh /usr/bin/
|
|
||||||
RUN chmod +x /usr/bin/entrypoint.sh
|
|
||||||
ENTRYPOINT ["entrypoint.sh"]
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Configure the main process to run when running the image
|
|
||||||
CMD ["bin", "dev"]
|
|
||||||
|
|||||||
8
Gemfile
8
Gemfile
@@ -32,12 +32,14 @@ gem 'lockbox'
|
|||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
gem 'warden'
|
gem 'warden'
|
||||||
gem 'devise'
|
gem 'devise', '~> 4.9.0'
|
||||||
gem 'devise_ldap_authenticatable'
|
gem 'devise_ldap_authenticatable'
|
||||||
gem 'net-ldap'
|
gem 'net-ldap'
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
gem "rqrcode", "~> 2.0"
|
gem "rqrcode", "~> 2.0"
|
||||||
|
gem 'rails-settings-cached', '~> 2.8.3'
|
||||||
|
gem 'pagy', '~> 6.0', '>= 6.0.2'
|
||||||
|
|
||||||
# HTTP requests
|
# HTTP requests
|
||||||
gem 'faraday'
|
gem 'faraday'
|
||||||
@@ -46,6 +48,10 @@ gem 'faraday'
|
|||||||
gem 'sidekiq', '< 7'
|
gem 'sidekiq', '< 7'
|
||||||
gem 'sidekiq-scheduler'
|
gem 'sidekiq-scheduler'
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
gem "sentry-ruby"
|
||||||
|
gem "sentry-rails"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use sqlite3 as the database for Active Record
|
||||||
gem 'sqlite3', '~> 1.4'
|
gem 'sqlite3', '~> 1.4'
|
||||||
|
|||||||
23
Gemfile.lock
23
Gemfile.lock
@@ -95,7 +95,7 @@ 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)
|
||||||
devise (4.8.1)
|
devise (4.9.0)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -178,6 +178,7 @@ GEM
|
|||||||
nokogiri (1.13.9-x86_64-linux)
|
nokogiri (1.13.9-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
pagy (6.0.2)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
public_suffix (5.0.0)
|
public_suffix (5.0.0)
|
||||||
puma (4.3.12)
|
puma (4.3.12)
|
||||||
@@ -206,6 +207,9 @@ GEM
|
|||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.3)
|
rails-html-sanitizer (1.4.3)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
|
rails-settings-cached (2.8.3)
|
||||||
|
activerecord (>= 5.0.0)
|
||||||
|
railties (>= 5.0.0)
|
||||||
railties (7.0.4)
|
railties (7.0.4)
|
||||||
actionpack (= 7.0.4)
|
actionpack (= 7.0.4)
|
||||||
activesupport (= 7.0.4)
|
activesupport (= 7.0.4)
|
||||||
@@ -222,9 +226,9 @@ GEM
|
|||||||
redis-client (0.11.2)
|
redis-client (0.11.2)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.6.1)
|
regexp_parser (2.6.1)
|
||||||
responders (3.0.1)
|
responders (3.1.0)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.0)
|
railties (>= 5.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rqrcode (2.1.2)
|
rqrcode (2.1.2)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@@ -250,6 +254,11 @@ GEM
|
|||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rufus-scheduler (3.8.2)
|
rufus-scheduler (3.8.2)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
|
sentry-rails (5.8.0)
|
||||||
|
railties (>= 5.0)
|
||||||
|
sentry-ruby (~> 5.8.0)
|
||||||
|
sentry-ruby (5.8.0)
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (6.5.5)
|
sidekiq (6.5.5)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
@@ -311,7 +320,7 @@ DEPENDENCIES
|
|||||||
capybara
|
capybara
|
||||||
cssbundling-rails
|
cssbundling-rails
|
||||||
database_cleaner
|
database_cleaner
|
||||||
devise
|
devise (~> 4.9.0)
|
||||||
devise_ldap_authenticatable
|
devise_ldap_authenticatable
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
@@ -324,11 +333,15 @@ DEPENDENCIES
|
|||||||
listen (~> 3.2)
|
listen (~> 3.2)
|
||||||
lockbox
|
lockbox
|
||||||
net-ldap
|
net-ldap
|
||||||
|
pagy (~> 6.0, >= 6.0.2)
|
||||||
pg (~> 1.2.3)
|
pg (~> 1.2.3)
|
||||||
puma (~> 4.1)
|
puma (~> 4.1)
|
||||||
rails (~> 7.0.2)
|
rails (~> 7.0.2)
|
||||||
|
rails-settings-cached (~> 2.8.3)
|
||||||
rqrcode (~> 2.0)
|
rqrcode (~> 2.0)
|
||||||
rspec-rails
|
rspec-rails
|
||||||
|
sentry-rails
|
||||||
|
sentry-ruby
|
||||||
sidekiq (< 7)
|
sidekiq (< 7)
|
||||||
sidekiq-scheduler
|
sidekiq-scheduler
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ so:
|
|||||||
|
|
||||||
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||||
Docker Desktop)
|
Docker Desktop)
|
||||||
2. Uncomment the `web` section in `docker-compose.yml`
|
2. Uncomment the `redis`, `web`, and `sidekiq` sections in `docker-compose.yml`
|
||||||
3. Run `docker compose up` and wait until 389ds announces its successful start
|
3. Run `docker compose up` and wait until 389ds announces its successful start
|
||||||
in the log output
|
in the log output
|
||||||
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
||||||
5. `docker compose run web rails ldap:setup`
|
5. `docker compose run web rails ldap:setup`
|
||||||
5. `docker compose run web rails db:setup`
|
6. `docker compose run web rails db:setup`
|
||||||
|
|
||||||
After these steps, you should have a working Rails app with a handful of test
|
After these steps, you should have a working Rails app with a handful of test
|
||||||
users running on [http://localhost:3000](http://localhost:3000).
|
users running on [http://localhost:3000](http://localhost:3000).
|
||||||
@@ -81,12 +81,15 @@ with a fresh installation, delete both that directory as well as the container.
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
### Rails
|
||||||
|
|
||||||
* [Ruby on Rails](https://guides.rubyonrails.org/)
|
* [Ruby on Rails](https://guides.rubyonrails.org/)
|
||||||
* [Sass](https://sass-lang.com/documentation)
|
* [Pagination](https://ddnexus.github.io/pagy/)
|
||||||
|
|
||||||
### Front-end
|
### Front-end
|
||||||
|
|
||||||
* [Tailwind CSS](https://tailwindcss.com/)
|
* [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
* [Sass](https://sass-lang.com/documentation)
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
@import "components/base";
|
@import "components/base";
|
||||||
@import "components/buttons";
|
@import "components/buttons";
|
||||||
|
@import "components/dashboard_services";
|
||||||
@import "components/forms";
|
@import "components/forms";
|
||||||
@import "components/links";
|
@import "components/links";
|
||||||
@import "components/notifications";
|
@import "components/notifications";
|
||||||
|
@import "components/pagination";
|
||||||
@import "components/tables";
|
@import "components/tables";
|
||||||
|
|||||||
@@ -36,10 +36,18 @@
|
|||||||
@apply mb-4 leading-6;
|
@apply mb-4 leading-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main p:last-child {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
main ul {
|
main ul {
|
||||||
@apply mb-6;
|
@apply mb-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main ul:last-child {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
main ul li {
|
main ul li {
|
||||||
@apply leading-6;
|
@apply leading-6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
.btn {
|
.btn {
|
||||||
@apply font-semibold rounded-md leading-none cursor-pointer text-center
|
@apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
|
||||||
transition-colors duration-75 focus:outline-none focus:ring-4;
|
transition-colors duration-75 focus:outline-none focus:ring-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
app/assets/stylesheets/components/dashboard_services.css
Normal file
5
app/assets/stylesheets/components/dashboard_services.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@layer components {
|
||||||
|
.services > div > a {
|
||||||
|
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
input[type=text], input[type=email], input[type=password],
|
input[type=text], input[type=email], input[type=password],
|
||||||
input[type=number], select {
|
input[type=number], select, textarea {
|
||||||
@apply mt-1 rounded-md bg-gray-100 focus:bg-white
|
@apply rounded-md bg-gray-100 focus:bg-white
|
||||||
border-transparent focus:border-transparent focus:ring-2
|
border-transparent focus:border-transparent focus:ring-2
|
||||||
focus:ring-blue-600 focus:ring-opacity-75;
|
focus:ring-blue-600 focus:ring-opacity-75;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
@apply inline-block;
|
@apply inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field_with_errors input {
|
||||||
|
@apply w-full bg-red-100;
|
||||||
|
}
|
||||||
|
|
||||||
.error-msg {
|
.error-msg {
|
||||||
@apply text-red-700;
|
@apply text-red-700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,4 @@
|
|||||||
&:visited { @apply text-indigo-600; }
|
&:visited { @apply text-indigo-600; }
|
||||||
&:active { @apply text-red-600; }
|
&:active { @apply text-red-600; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.devise-links {
|
|
||||||
a {
|
|
||||||
@apply ks-text-link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
45
app/assets/stylesheets/components/pagination.css
Normal file
45
app/assets/stylesheets/components/pagination.css
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@layer components {
|
||||||
|
.pagy-nav.pagination {
|
||||||
|
@apply isolate inline-flex -space-x-px rounded-md shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page:not(.prev):not(.next) {
|
||||||
|
@apply hidden sm:inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page.next a {
|
||||||
|
@apply relative inline-flex items-center rounded-r-md border
|
||||||
|
border-gray-300 bg-white px-3 py-2 text-sm font-medium
|
||||||
|
text-gray-500 hover:bg-gray-100 focus:z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page.prev a {
|
||||||
|
@apply relative inline-flex items-center rounded-l-md border
|
||||||
|
border-gray-300 bg-white px-3 py-2 text-sm font-medium
|
||||||
|
text-gray-500 hover:bg-gray-100 focus:z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page.next.disabled {
|
||||||
|
@apply relative inline-flex items-center rounded-r-md border
|
||||||
|
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
|
||||||
|
text-gray-400 focus:z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page.prev.disabled {
|
||||||
|
@apply relative inline-flex items-center rounded-l-md border
|
||||||
|
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
|
||||||
|
text-gray-400 focus:z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page a, .page.gap {
|
||||||
|
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
|
||||||
|
inline-flex items-center border px-4 py-2 text-sm font-medium
|
||||||
|
focus:z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagy-nav .page.active {
|
||||||
|
@apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative
|
||||||
|
inline-flex items-center border px-4 py-2 text-sm font-medium
|
||||||
|
focus:z-20;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,30 @@
|
|||||||
@apply text-left;
|
@apply text-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
table th {
|
table thead th {
|
||||||
@apply pb-3.5 text-sm font-normal uppercase text-gray-500;
|
@apply pb-3.5 text-sm font-normal uppercase text-gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table tbody th {
|
||||||
|
@apply text-left font-normal text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
table th:not(:last-of-type),
|
table th:not(:last-of-type),
|
||||||
table td:not(:last-of-type) {
|
table td:not(:last-of-type) {
|
||||||
@apply pr-2;
|
@apply pr-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
table td {
|
table td, tbody th {
|
||||||
@apply py-2;
|
@apply py-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.divided {
|
||||||
|
@apply divide-y divide-gray-300;
|
||||||
|
}
|
||||||
|
table.divided tbody {
|
||||||
|
@apply divide-y divide-gray-200;
|
||||||
|
}
|
||||||
|
table.divided td, table.divided tbody th {
|
||||||
|
@apply py-3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
app/components/form_elements/fieldset_component.html.erb
Normal file
13
app/components/form_elements/fieldset_component.html.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
|
||||||
|
<label class="block">
|
||||||
|
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
|
||||||
|
<%= @title %>
|
||||||
|
</p>
|
||||||
|
<% if @descripton.present? %>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
<%= @descripton %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<%= content %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
11
app/components/form_elements/fieldset_component.rb
Normal file
11
app/components/form_elements/fieldset_component.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FormElements
|
||||||
|
class FieldsetComponent < ViewComponent::Base
|
||||||
|
def initialize(tag: "li", title:, description: nil)
|
||||||
|
@tag = tag
|
||||||
|
@title = title
|
||||||
|
@descripton = description
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
|
||||||
|
data: @form.present? ? {
|
||||||
|
controller: "settings--toggle",
|
||||||
|
:'settings--toggle-switch-enabled-value' => @enabled.to_s
|
||||||
|
} : nil do %>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="font-bold mb-1"><%= @title %></label>
|
||||||
|
<p class="text-gray-500"><%= @descripton %></p>
|
||||||
|
</div>
|
||||||
|
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @enabled,
|
||||||
|
input_enabled: @input_enabled,
|
||||||
|
class_names: @form.present? ? "hidden" : nil,
|
||||||
|
data: {
|
||||||
|
:'settings--toggle-target' => "button",
|
||||||
|
action: "settings--toggle#toggleSwitch"
|
||||||
|
}) %>
|
||||||
|
<% if @form.present? %>
|
||||||
|
<%= @form.check_box @attribute, {
|
||||||
|
checked: @enabled,
|
||||||
|
data: { :'settings--toggle-target' => "checkbox" }
|
||||||
|
}, "true", "false" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
17
app/components/form_elements/fieldset_toggle_component.rb
Normal file
17
app/components/form_elements/fieldset_toggle_component.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FormElements
|
||||||
|
class FieldsetToggleComponent < ViewComponent::Base
|
||||||
|
def initialize(form: nil, attribute: nil, tag: "li", enabled: false,
|
||||||
|
input_enabled: true, title:, description:)
|
||||||
|
@form = form
|
||||||
|
@attribute = attribute
|
||||||
|
@tag = tag
|
||||||
|
@enabled = enabled
|
||||||
|
@input_enabled = input_enabled
|
||||||
|
@title = title
|
||||||
|
@descripton = description
|
||||||
|
@button_text = @enabled ? "Switch off" : "Switch on"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
15
app/components/form_elements/toggle_component.html.erb
Normal file
15
app/components/form_elements/toggle_component.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<%= button_tag type: "button", name: "toggle", data: @data,
|
||||||
|
role: "switch", aria: { checked: @enabled.to_s },
|
||||||
|
tabindex: @tabindex, disabled: !@input_enabled,
|
||||||
|
class: "#{ @enabled ? 'bg-blue-600' : 'bg-gray-200' }
|
||||||
|
#{ @class_names.present? ? @class_names : '' }
|
||||||
|
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer
|
||||||
|
rounded-full border-2 border-transparent transition-colors
|
||||||
|
duration-200 ease-in-out focus:outline-none focus:ring-2
|
||||||
|
focus:ring-blue-600 focus:ring-offset-2" do %>
|
||||||
|
<span class="sr-only"><%= @button_text %></span>
|
||||||
|
<span aria-hidden="true" data-settings--toggle-target="switch"
|
||||||
|
class="<%= @enabled ? 'translate-x-5' : 'translate-x-0' %>
|
||||||
|
pointer-events-none inline-block h-5 w-5 transform rounded-full
|
||||||
|
bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||||
|
<% end %>
|
||||||
13
app/components/form_elements/toggle_component.rb
Normal file
13
app/components/form_elements/toggle_component.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FormElements
|
||||||
|
class ToggleComponent < ViewComponent::Base
|
||||||
|
def initialize(enabled:, input_enabled: true, data: nil, class_names: nil, tabindex: nil)
|
||||||
|
@enabled = !!enabled
|
||||||
|
@input_enabled = input_enabled
|
||||||
|
@data = data
|
||||||
|
@class_names = class_names
|
||||||
|
@tabindex = tabindex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
3
app/components/quickstats_container_component.html.erb
Normal file
3
app/components/quickstats_container_component.html.erb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<dl class="grid grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-12">
|
||||||
|
<%= content %>
|
||||||
|
</dl>
|
||||||
4
app/components/quickstats_container_component.rb
Normal file
4
app/components/quickstats_container_component.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class QuickstatsContainerComponent < ViewComponent::Base
|
||||||
|
end
|
||||||
18
app/components/quickstats_item_component.html.erb
Normal file
18
app/components/quickstats_item_component.html.erb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="">
|
||||||
|
<dt class="mb-2 text-gray-500">
|
||||||
|
<%= @title %>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<% if @type == :number %>
|
||||||
|
<span class="text-2xl"><%= number_with_delimiter @value %></span>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-2xl"><%= @value %></span>
|
||||||
|
<% end %>
|
||||||
|
<% if @unit %>
|
||||||
|
<span><%= @unit %></span>
|
||||||
|
<% end %>
|
||||||
|
<% if @meta %>
|
||||||
|
<span class="text-gray-500"><%= @meta %></span>
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
13
app/components/quickstats_item_component.rb
Normal file
13
app/components/quickstats_item_component.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class QuickstatsItemComponent < ViewComponent::Base
|
||||||
|
def initialize(type:, title:, value:, unit: nil, meta: nil, icon_name: nil, icon_color_class: nil)
|
||||||
|
@type = type
|
||||||
|
@title = title
|
||||||
|
@value = value
|
||||||
|
@unit = unit
|
||||||
|
@meta = meta
|
||||||
|
@icon_name = icon_name
|
||||||
|
@icon_color_class = icon_color_class
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SidenavLinkComponent < ViewComponent::Base
|
class SidenavLinkComponent < ViewComponent::Base
|
||||||
def initialize(name:, path:, icon:, active: false, disabled: false)
|
def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
|
||||||
@name = name
|
@name = name
|
||||||
|
@level = level
|
||||||
@path = path
|
@path = path
|
||||||
@icon = icon
|
@icon = icon
|
||||||
@active = active
|
@active = active
|
||||||
@@ -12,12 +13,15 @@ class SidenavLinkComponent < ViewComponent::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def class_names_link(path)
|
def class_names_link(path)
|
||||||
|
px = @level == 1 ? "px-4" : "pl-8 pr-4"
|
||||||
|
base = "#{px} py-2 group border-l-4 flex items-center text-base font-medium"
|
||||||
|
|
||||||
if @active
|
if @active
|
||||||
"bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700 group border-l-4 px-4 py-2 flex items-center text-base font-medium"
|
"#{base} bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
|
||||||
elsif @disabled
|
elsif @disabled
|
||||||
"border-transparent text-gray-400 hover:bg-gray-50 group border-l-4 px-4 py-2 flex items-center text-base font-medium"
|
"#{base} border-transparent text-gray-400 hover:bg-gray-50"
|
||||||
else
|
else
|
||||||
"border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-4 py-2 flex items-center text-base font-medium"
|
"#{base} border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,14 @@
|
|||||||
<div class="md:col-span-4 mt-4 md:mt-0">
|
<div class="md:col-span-4 mt-4 md:mt-0">
|
||||||
<p class="font-mono md:text-right mb-0 p-4 border border-gray-300 rounded-lg overflow-hidden">
|
<p class="font-mono md:text-right mb-0 p-4 border border-gray-300 rounded-lg overflow-hidden">
|
||||||
<% if @balance %>
|
<% if @balance %>
|
||||||
<span class="text-xl"><%= number_with_delimiter @balance %> sats</span><br>
|
<span class="text-2xl"><%= number_with_delimiter @balance %></span>
|
||||||
|
<span class="text-xl">sats</span>
|
||||||
|
<br>
|
||||||
<span class="text-sm text-gray-500">Available balance</span>
|
<span class="text-sm text-gray-500">Available balance</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="text-xl">n/a sats</span><br>
|
<span class="text-2xl">n/a</span>
|
||||||
|
<span class="text-xl">sats</span>
|
||||||
|
<br>
|
||||||
<span class="text-sm text-gray-500">Balance unavailable</span>
|
<span class="text-sm text-gray-500">Balance unavailable</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Admin::BaseController < ApplicationController
|
class Admin::BaseController < ApplicationController
|
||||||
|
include Pagy::Backend
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :authorize_admin
|
before_action :authorize_admin
|
||||||
@@ -7,5 +8,4 @@ class Admin::BaseController < ApplicationController
|
|||||||
def set_context
|
def set_context
|
||||||
@context = :admin
|
@context = :admin
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
# GET /donations
|
# GET /donations
|
||||||
# GET /donations.json
|
# GET /donations.json
|
||||||
def index
|
def index
|
||||||
@donations = Donation.all
|
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
|
||||||
|
|
||||||
|
@stats = {
|
||||||
|
overall_sats: @donations.all.sum("amount_sats"),
|
||||||
|
donor_count: Donation.distinct.count(:user_id)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /donations/1
|
# GET /donations/1
|
||||||
@@ -29,10 +34,14 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @donation.save
|
if @donation.save
|
||||||
format.html { redirect_to admin_donation_url(@donation), notice: 'Donation was successfully created.' }
|
format.html do
|
||||||
|
redirect_to admin_donation_url(@donation), flash: {
|
||||||
|
success: 'Donation was successfully created.'
|
||||||
|
}
|
||||||
|
end
|
||||||
format.json { render :show, status: :created, location: @donation }
|
format.json { render :show, status: :created, location: @donation }
|
||||||
else
|
else
|
||||||
format.html { render :new }
|
format.html { render :new, status: :unprocessable_entity }
|
||||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -43,10 +52,14 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
def update
|
def update
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @donation.update(donation_params)
|
if @donation.update(donation_params)
|
||||||
format.html { redirect_to admin_donation_url(@donation), notice: 'Donation was successfully updated.' }
|
format.html do
|
||||||
|
redirect_to admin_donation_url(@donation), flash: {
|
||||||
|
success: 'Donation was successfully updated.'
|
||||||
|
}
|
||||||
|
end
|
||||||
format.json { render :show, status: :ok, location: @donation }
|
format.json { render :show, status: :ok, location: @donation }
|
||||||
else
|
else
|
||||||
format.html { render :edit }
|
format.html { render :edit, status: :unprocessable_entity }
|
||||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -57,7 +70,10 @@ class Admin::DonationsController < Admin::BaseController
|
|||||||
def destroy
|
def destroy
|
||||||
@donation.destroy
|
@donation.destroy
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to admin_donations_url, notice: 'Donation was successfully destroyed.' }
|
format.html do redirect_to admin_donations_url, flash: {
|
||||||
|
success: 'Donation was successfully destroyed.'
|
||||||
|
}
|
||||||
|
end
|
||||||
format.json { head :no_content }
|
format.json { head :no_content }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
class Admin::InvitationsController < Admin::BaseController
|
class Admin::InvitationsController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
@current_section = :invitations
|
@current_section = :invitations
|
||||||
@invitations_unused_count = Invitation.unused.count
|
@pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
|
||||||
@users_with_referrals_count = Invitation.used.distinct.count(:user_id)
|
|
||||||
@invitations_used = Invitation.used.order('used_at desc')
|
@stats = {
|
||||||
|
available: Invitation.unused.count,
|
||||||
|
accepted: @invitations_used.length,
|
||||||
|
users_with_referrals: Invitation.used.distinct.count(:user_id)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
class Admin::LdapUsersController < Admin::BaseController
|
|
||||||
before_action :set_current_section
|
|
||||||
|
|
||||||
def index
|
|
||||||
attributes = %w{dn cn uid mail admin}
|
|
||||||
filter = Net::LDAP::Filter.eq("uid", "*")
|
|
||||||
|
|
||||||
@ou = params[:ou] || "kosmos.org"
|
|
||||||
treebase = "ou=#{@ou},cn=users,dc=kosmos,dc=org"
|
|
||||||
|
|
||||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
|
||||||
entries.sort_by! { |e| e.cn[0] }
|
|
||||||
|
|
||||||
@entries = entries.collect do |e|
|
|
||||||
{
|
|
||||||
uid: e.uid.first,
|
|
||||||
mail: e.try(:mail) ? e.mail.first : nil,
|
|
||||||
admin: e.try(:admin) ? 'admin' : nil
|
|
||||||
# password: e.userpassword.first
|
|
||||||
}
|
|
||||||
end
|
|
||||||
# ldap_client.get_operation_result
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def ldap_client
|
|
||||||
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
|
||||||
port: ldap_config['port'],
|
|
||||||
# encryption: ldap_config['ssl'],
|
|
||||||
auth: {
|
|
||||||
method: :simple,
|
|
||||||
username: ldap_config['admin_user'],
|
|
||||||
password: ldap_config['admin_password']
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def ldap_config
|
|
||||||
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_current_section
|
|
||||||
@current_section = :ldap_users
|
|
||||||
end
|
|
||||||
end
|
|
||||||
21
app/controllers/admin/lightning_controller.rb
Normal file
21
app/controllers/admin/lightning_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class Admin::LightningController < Admin::BaseController
|
||||||
|
before_action :check_feature_enabled
|
||||||
|
|
||||||
|
def index
|
||||||
|
@current_section = :lightning
|
||||||
|
|
||||||
|
@users = User.pluck(:cn, :ou, :ln_account)
|
||||||
|
@accounts = LndhubAccount.with_balances.order(balance: :desc).to_a
|
||||||
|
|
||||||
|
@ln = {}
|
||||||
|
@ln[:current_balance] = LndhubAccount.current.joins(:ledgers).sum("account_ledgers.amount")
|
||||||
|
@ln[:users_with_sats] = @accounts.length
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_feature_enabled
|
||||||
|
if !Setting.lndhub_admin_enabled?
|
||||||
|
flash[:alert] = "Lightning Admin UI not enabled"
|
||||||
|
redirect_to admin_root_path and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
app/controllers/admin/settings/registrations_controller.rb
Normal file
12
app/controllers/admin/settings/registrations_controller.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||||
|
def index
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
update_settings
|
||||||
|
|
||||||
|
redirect_to admin_settings_registrations_path, flash: {
|
||||||
|
success: "Settings saved"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
19
app/controllers/admin/settings/services_controller.rb
Normal file
19
app/controllers/admin/settings/services_controller.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
class Admin::Settings::ServicesController < Admin::SettingsController
|
||||||
|
def index
|
||||||
|
@service = params[:s]
|
||||||
|
|
||||||
|
if @service.blank?
|
||||||
|
redirect_to admin_settings_services_path(params: { s: "discourse" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
service = params.require(:service)
|
||||||
|
|
||||||
|
update_settings
|
||||||
|
|
||||||
|
redirect_to admin_settings_services_path(params: { s: service }), flash: {
|
||||||
|
success: "Settings saved"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
40
app/controllers/admin/settings_controller.rb
Normal file
40
app/controllers/admin/settings_controller.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
class Admin::SettingsController < Admin::BaseController
|
||||||
|
before_action :set_current_section
|
||||||
|
|
||||||
|
def index
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_settings
|
||||||
|
@errors = ActiveModel::Errors.new(Setting.new)
|
||||||
|
changed_keys = []
|
||||||
|
|
||||||
|
setting_params.keys.each do |key|
|
||||||
|
next if setting_params[key].nil? ||
|
||||||
|
(Setting.send(key).to_s == setting_params[key].strip)
|
||||||
|
changed_keys.push(key)
|
||||||
|
setting = Setting.new(var: key)
|
||||||
|
setting.value = setting_params[key].strip
|
||||||
|
unless setting.valid?
|
||||||
|
@errors.merge!(setting.errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @errors.any?
|
||||||
|
render :index and return
|
||||||
|
end
|
||||||
|
|
||||||
|
changed_keys.each do |key|
|
||||||
|
Setting.send("#{key}=", setting_params[key].strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_section
|
||||||
|
@current_section = :settings
|
||||||
|
end
|
||||||
|
|
||||||
|
def setting_params
|
||||||
|
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
|
||||||
|
end
|
||||||
|
end
|
||||||
35
app/controllers/admin/users_controller.rb
Normal file
35
app/controllers/admin/users_controller.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
class Admin::UsersController < Admin::BaseController
|
||||||
|
before_action :set_user, only: [:show]
|
||||||
|
before_action :set_current_section
|
||||||
|
|
||||||
|
def index
|
||||||
|
ldap = LdapService.new
|
||||||
|
@ou = params[:ou] || "kosmos.org"
|
||||||
|
@orgs = ldap.fetch_organizations
|
||||||
|
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
|
||||||
|
|
||||||
|
@stats = {
|
||||||
|
users_confirmed: User.where(ou: @ou).confirmed.count,
|
||||||
|
users_pending: User.where(ou: @ou).pending.count
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
if Setting.lndhub_admin_enabled?
|
||||||
|
@lndhub_user = @user.lndhub_user
|
||||||
|
end
|
||||||
|
|
||||||
|
@services_enabled = @user.services_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
address = params[:address].split("@")
|
||||||
|
@user = User.where(cn: address.first, ou: address.last).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_current_section
|
||||||
|
@current_section = :users
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,18 @@ class ApplicationController < ActionController::Base
|
|||||||
render :text => exception, :status => 500
|
render :text => exception, :status => 500
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_action :sentry_set_user
|
||||||
|
|
||||||
|
def sentry_set_user
|
||||||
|
return unless Setting.sentry_enabled
|
||||||
|
|
||||||
|
if user_signed_in?
|
||||||
|
Sentry.set_user(id: current_user.id, username: current_user.cn)
|
||||||
|
else
|
||||||
|
Sentry.set_user({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def require_user_signed_in
|
def require_user_signed_in
|
||||||
unless user_signed_in?
|
unless user_signed_in?
|
||||||
redirect_to welcome_path and return
|
redirect_to welcome_path and return
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class InvitationsController < ApplicationController
|
|||||||
# GET /invitations
|
# GET /invitations
|
||||||
def index
|
def index
|
||||||
@invitations_unused = current_user.invitations.unused
|
@invitations_unused = current_user.invitations.unused
|
||||||
@invitations_used = current_user.invitations.used
|
@invitations_used = current_user.invitations.used.order('used_at desc')
|
||||||
@current_section = :invitations
|
@current_section = :invitations
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -27,7 +27,10 @@ class InvitationsController < ApplicationController
|
|||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @invitation.save
|
if @invitation.save
|
||||||
format.html { redirect_to @invitation, notice: 'Invitation was successfully created.' }
|
format.html do redirect_to @invitation, flash: {
|
||||||
|
success: 'Invitation was successfully created.'
|
||||||
|
}
|
||||||
|
end
|
||||||
format.json { render :show, status: :created, location: @invitation }
|
format.json { render :show, status: :created, location: @invitation }
|
||||||
else
|
else
|
||||||
format.html { render :new }
|
format.html { render :new }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class LnurlpayController < ApplicationController
|
class LnurlpayController < ApplicationController
|
||||||
|
before_action :check_feature_enabled
|
||||||
before_action :find_user_by_address
|
before_action :find_user_by_address
|
||||||
|
|
||||||
MIN_SATS = 10
|
MIN_SATS = 10
|
||||||
@@ -17,6 +18,20 @@ class LnurlpayController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def keysend
|
||||||
|
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
status: "OK",
|
||||||
|
tag: "keysend",
|
||||||
|
pubkey: Setting.lndhub_public_key,
|
||||||
|
customData: [{
|
||||||
|
customKey: "696969",
|
||||||
|
customValue: @user.ln_account
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def invoice
|
def invoice
|
||||||
amount = params[:amount].to_i / 1000 # msats
|
amount = params[:amount].to_i / 1000 # msats
|
||||||
address = params[:address]
|
address = params[:address]
|
||||||
@@ -32,7 +47,7 @@ class LnurlpayController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
memo = "Sats for #{address}"
|
memo = "To #{address}"
|
||||||
memo = "#{memo}: \"#{comment}\"" if comment.present?
|
memo = "#{memo}: \"#{comment}\"" if comment.present?
|
||||||
|
|
||||||
payment_request = @user.ln_create_invoice({
|
payment_request = @user.ln_create_invoice({
|
||||||
@@ -72,4 +87,9 @@ class LnurlpayController < ApplicationController
|
|||||||
comment.length <= MAX_COMMENT_CHARS
|
comment.length <= MAX_COMMENT_CHARS
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_feature_enabled
|
||||||
|
http_status :not_found unless Setting.lndhub_enabled?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
17
app/controllers/users/confirmations_controller.rb
Normal file
17
app/controllers/users/confirmations_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Users::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
# GET /resource/confirmation?confirmation_token=abcdef
|
||||||
|
def show
|
||||||
|
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
|
||||||
|
yield resource if block_given?
|
||||||
|
|
||||||
|
if resource.errors.empty?
|
||||||
|
set_flash_message!(:success, :confirmed)
|
||||||
|
resource.devise_after_confirmation
|
||||||
|
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
|
||||||
|
else
|
||||||
|
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,7 +7,7 @@ class WalletController < ApplicationController
|
|||||||
before_action :fetch_balance
|
before_action :fetch_balance
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@wallet_url = "lndhub://#{current_user.ln_login}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
@wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||||
|
|
||||||
qrcode = RQRCode::QRCode.new(@wallet_url)
|
qrcode = RQRCode::QRCode.new(@wallet_url)
|
||||||
@svg = qrcode.as_svg(
|
@svg = qrcode.as_svg(
|
||||||
|
|||||||
40
app/controllers/webhooks_controller.rb
Normal file
40
app/controllers/webhooks_controller.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
class WebhooksController < ApplicationController
|
||||||
|
skip_forgery_protection
|
||||||
|
|
||||||
|
before_action :authorize_request
|
||||||
|
|
||||||
|
def lndhub
|
||||||
|
begin
|
||||||
|
payload = JSON.parse(request.body.read, symbolize_names: true)
|
||||||
|
head :no_content and return unless payload[:type] == "incoming"
|
||||||
|
rescue
|
||||||
|
head :unprocessable_entity and return
|
||||||
|
end
|
||||||
|
|
||||||
|
user = User.find_by!(ln_account: payload[:user_login])
|
||||||
|
|
||||||
|
# TODO make configurable
|
||||||
|
notify_xmpp(user.address, payload[:amount], payload[:memo])
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def notify_xmpp(address, amt_sats, memo)
|
||||||
|
payload = {
|
||||||
|
type: "normal",
|
||||||
|
from: "kosmos.org", # TODO domain config
|
||||||
|
to: address,
|
||||||
|
subject: "Sats received!",
|
||||||
|
body: "#{amt_sats} sats received in your Lightning wallet:\n> #{memo}"
|
||||||
|
}
|
||||||
|
XmppSendMessageJob.perform_later(payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_request
|
||||||
|
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
|
||||||
|
head :forbidden and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
|
include Pagy::Frontend
|
||||||
|
|
||||||
def sats_to_btc(sats)
|
def sats_to_btc(sats)
|
||||||
sats.to_f / 100000000
|
sats.to_f / 100000000
|
||||||
end
|
end
|
||||||
@@ -10,5 +12,10 @@ module ApplicationHelper
|
|||||||
"text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
|
"text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
# Colors available: gray, red, yellow, green, blue, purple, pink
|
||||||
|
# (Add more colors by adding classes to the safelist in tailwind.config.js)
|
||||||
|
def badge(text, color)
|
||||||
|
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
module LdapUsersHelper
|
|
||||||
end
|
|
||||||
2
app/helpers/users_helper.rb
Normal file
2
app/helpers/users_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module UsersHelper
|
||||||
|
end
|
||||||
@@ -4,6 +4,10 @@ export default class extends Controller {
|
|||||||
static targets = ["buttons", "countdown"]
|
static targets = ["buttons", "countdown"]
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
// Devise timeoutable ends up adding a second flash message without content
|
||||||
|
// TODO investigate bug
|
||||||
|
if (this.element.textContent.trim() == "true") return;
|
||||||
|
|
||||||
const timeoutSeconds = parseInt(this.data.get("timeout"));
|
const timeoutSeconds = parseInt(this.data.get("timeout"));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
30
app/javascript/controllers/settings/toggle_controller.js
Normal file
30
app/javascript/controllers/settings/toggle_controller.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "button", "switch", "checkbox" ]
|
||||||
|
static values = { switchEnabled: Boolean }
|
||||||
|
|
||||||
|
connect () {
|
||||||
|
this.buttonTarget.classList.remove("hidden")
|
||||||
|
this.checkboxTarget.classList.add("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSwitch () {
|
||||||
|
this.switchEnabledValue = !this.switchEnabledValue
|
||||||
|
this.checkboxTarget.checked = this.switchEnabledValue
|
||||||
|
|
||||||
|
if (this.switchEnabledValue) {
|
||||||
|
this.buttonTarget.setAttribute("aria-checked", "true");
|
||||||
|
this.buttonTarget.classList.remove("bg-gray-200")
|
||||||
|
this.buttonTarget.classList.add("bg-blue-600")
|
||||||
|
this.switchTarget.classList.remove("translate-x-0")
|
||||||
|
this.switchTarget.classList.add("translate-x-5")
|
||||||
|
} else {
|
||||||
|
this.buttonTarget.setAttribute("aria-checked", "false");
|
||||||
|
this.buttonTarget.classList.remove("bg-blue-600")
|
||||||
|
this.buttonTarget.classList.add("bg-gray-200")
|
||||||
|
this.switchTarget.classList.remove("translate-x-5")
|
||||||
|
this.switchTarget.classList.add("translate-x-0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
|
|||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(user)
|
def perform(user)
|
||||||
return if user.ln_login.present? && user.ln_password.present?
|
return if user.ln_account.present? && user.ln_password.present?
|
||||||
|
|
||||||
lndhub = LndhubV2.new
|
lndhub = LndhubV2.new
|
||||||
credentials = lndhub.create_account
|
credentials = lndhub.create_account
|
||||||
|
|
||||||
user.update! ln_login: credentials["login"],
|
user.update! ln_account: credentials["login"],
|
||||||
ln_password: credentials["password"]
|
ln_password: credentials["password"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class ExchangeXmppContactsJob < ApplicationJob
|
class XmppExchangeContactsJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(inviter, username, domain)
|
def perform(inviter, username, domain)
|
||||||
@@ -7,12 +7,12 @@ class ExchangeXmppContactsJob < ApplicationJob
|
|||||||
ejabberd.add_rosteritem({
|
ejabberd.add_rosteritem({
|
||||||
"localuser": username, "localhost": domain,
|
"localuser": username, "localhost": domain,
|
||||||
"user": inviter.cn, "host": inviter.ou,
|
"user": inviter.cn, "host": inviter.ou,
|
||||||
"nick": inviter.cn, "group": "Friends", "subs": "both"
|
"nick": inviter.cn, "group": Setting.ejabberd_buddy_roster, "subs": "both"
|
||||||
})
|
})
|
||||||
ejabberd.add_rosteritem({
|
ejabberd.add_rosteritem({
|
||||||
"localuser": inviter.cn, "localhost": inviter.ou,
|
"localuser": inviter.cn, "localhost": inviter.ou,
|
||||||
"user": username, "host": domain,
|
"user": username, "host": domain,
|
||||||
"nick": username, "group": "Friends", "subs": "both"
|
"nick": username, "group": Setting.ejabberd_buddy_roster, "subs": "both"
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
8
app/jobs/xmpp_send_message_job.rb
Normal file
8
app/jobs/xmpp_send_message_job.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class XmppSendMessageJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(payload)
|
||||||
|
ejabberd = EjabberdApiClient.new
|
||||||
|
ejabberd.send_message payload
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: 'from@example.com'
|
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
end
|
end
|
||||||
|
|||||||
23
app/mailers/custom_mailer.rb
Normal file
23
app/mailers/custom_mailer.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# A custom mailer that can be used from the Rails console for one-off emails
|
||||||
|
# today, and later connected from an admin panel mailing page.
|
||||||
|
#
|
||||||
|
# Assign any template variables you want to use:
|
||||||
|
#
|
||||||
|
# user = User.first
|
||||||
|
#
|
||||||
|
# Create the email body from a custom email template file:
|
||||||
|
#
|
||||||
|
# body = ERB.new(File.read('./tmp/mailer-1.txt.erb')).result binding
|
||||||
|
#
|
||||||
|
# Send email via Sidekiq:
|
||||||
|
#
|
||||||
|
# CustomMailer.with(user: user, subject: "Important announcement", body: body).custom_message.deliver_later
|
||||||
|
#
|
||||||
|
class CustomMailer < ApplicationMailer
|
||||||
|
def custom_message
|
||||||
|
@user = params[:user]
|
||||||
|
@subject = params[:subject]
|
||||||
|
@body = params[:body]
|
||||||
|
mail(to: @user.email, subject: @subject)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,9 @@ class Donation < ApplicationRecord
|
|||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
# Validations
|
# Validations
|
||||||
|
validates_presence_of :user
|
||||||
validates_presence_of :amount_sats
|
validates_presence_of :amount_sats
|
||||||
|
validates_presence_of :paid_at
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
# TODO before_create :store_fiat_value
|
# TODO before_create :store_fiat_value
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Invitation < ApplicationRecord
|
class Invitation < ApplicationRecord
|
||||||
# Relations
|
# Relations
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
belongs_to :invitee, class_name: "User", foreign_key: 'invited_user_id', optional: true
|
||||||
|
|
||||||
# Validations
|
# Validations
|
||||||
validates_presence_of :user
|
validates_presence_of :user
|
||||||
|
|||||||
21
app/models/lndhub_account.rb
Normal file
21
app/models/lndhub_account.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class LndhubAccount < LndhubBase
|
||||||
|
self.table_name = "accounts"
|
||||||
|
self.inheritance_column = :_type_disabled
|
||||||
|
|
||||||
|
has_many :ledgers, class_name: "LndhubAccountLedger",
|
||||||
|
foreign_key: "account_id"
|
||||||
|
|
||||||
|
belongs_to :user, class_name: "LndhubUser",
|
||||||
|
foreign_key: "user_id"
|
||||||
|
|
||||||
|
scope :current, -> { where(type: "current") }
|
||||||
|
scope :outgoing, -> { where(type: "outgoing") }
|
||||||
|
scope :incoming, -> { where(type: "incoming") }
|
||||||
|
scope :fees, -> { where(type: "fees") }
|
||||||
|
|
||||||
|
scope :with_balances, -> {
|
||||||
|
current.joins(:user).joins(:ledgers)
|
||||||
|
.group("accounts.id", "users.login")
|
||||||
|
.select("accounts.id, users.login, SUM(account_ledgers.amount) AS balance")
|
||||||
|
}
|
||||||
|
end
|
||||||
3
app/models/lndhub_account_ledger.rb
Normal file
3
app/models/lndhub_account_ledger.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class LndhubAccountLedger < LndhubBase
|
||||||
|
self.table_name = "account_ledgers"
|
||||||
|
end
|
||||||
4
app/models/lndhub_base.rb
Normal file
4
app/models/lndhub_base.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
class LndhubBase < ActiveRecord::Base
|
||||||
|
self.abstract_class = true
|
||||||
|
establish_connection :lndhub
|
||||||
|
end
|
||||||
27
app/models/lndhub_user.rb
Normal file
27
app/models/lndhub_user.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
class LndhubUser < LndhubBase
|
||||||
|
self.table_name = "users"
|
||||||
|
self.inheritance_column = :_type_disabled
|
||||||
|
|
||||||
|
has_many :accounts, class_name: "LndhubAccount",
|
||||||
|
foreign_key: "user_id"
|
||||||
|
|
||||||
|
belongs_to :user, class_name: "User",
|
||||||
|
primary_key: "ln_account",
|
||||||
|
foreign_key: "login"
|
||||||
|
|
||||||
|
def balance
|
||||||
|
accounts.current.first.ledgers.sum("account_ledgers.amount").to_i.abs
|
||||||
|
end
|
||||||
|
|
||||||
|
def sum_outgoing
|
||||||
|
accounts.outgoing.first.ledgers.sum("account_ledgers.amount").to_i.abs
|
||||||
|
end
|
||||||
|
|
||||||
|
def sum_incoming
|
||||||
|
accounts.incoming.first.ledgers.sum("account_ledgers.amount").to_i.abs
|
||||||
|
end
|
||||||
|
|
||||||
|
def sum_fees
|
||||||
|
accounts.fees.first.ledgers.sum("account_ledgers.amount").to_i.abs
|
||||||
|
end
|
||||||
|
end
|
||||||
107
app/models/setting.rb
Normal file
107
app/models/setting.rb
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# RailsSettings Model
|
||||||
|
class Setting < RailsSettings::Base
|
||||||
|
cache_prefix { "v1" }
|
||||||
|
|
||||||
|
#
|
||||||
|
# Internal services
|
||||||
|
#
|
||||||
|
|
||||||
|
field :redis_url, type: :string, readonly: true,
|
||||||
|
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Registrations
|
||||||
|
#
|
||||||
|
|
||||||
|
field :reserved_usernames, type: :array, default: %w[
|
||||||
|
account accounts donations mail webmaster support
|
||||||
|
]
|
||||||
|
|
||||||
|
#
|
||||||
|
# Sentry
|
||||||
|
#
|
||||||
|
|
||||||
|
field :sentry_enabled, type: :boolean, readonly: true,
|
||||||
|
default: (ENV["SENTRY_DSN"].present?.to_s || false)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Discourse
|
||||||
|
#
|
||||||
|
|
||||||
|
field :discourse_public_url, type: :string, readonly: true,
|
||||||
|
default: ENV["DISCOURSE_PUBLIC_URL"].presence
|
||||||
|
|
||||||
|
field :discourse_enabled, type: :boolean,
|
||||||
|
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
#
|
||||||
|
# ejabberd
|
||||||
|
#
|
||||||
|
|
||||||
|
field :ejabberd_enabled, type: :boolean,
|
||||||
|
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
field :ejabberd_api_url, type: :string, readonly: true,
|
||||||
|
default: ENV["EJABBERD_API_URL"].presence
|
||||||
|
|
||||||
|
field :ejabberd_admin_url, type: :string, readonly: true,
|
||||||
|
default: ENV["EJABBERD_ADMIN_URL"].presence
|
||||||
|
|
||||||
|
field :ejabberd_buddy_roster, type: :string,
|
||||||
|
default: "Buddies"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Gitea
|
||||||
|
#
|
||||||
|
|
||||||
|
field :gitea_public_url, type: :string, readonly: true,
|
||||||
|
default: ENV["GITEA_PUBLIC_URL"].presence
|
||||||
|
|
||||||
|
field :gitea_enabled, type: :boolean,
|
||||||
|
default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Lightning Network
|
||||||
|
#
|
||||||
|
|
||||||
|
field :lndhub_api_url, type: :string, readonly: true,
|
||||||
|
default: ENV["LNDHUB_API_URL"].presence
|
||||||
|
|
||||||
|
field :lndhub_enabled, type: :boolean,
|
||||||
|
default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
field :lndhub_admin_enabled, type: :boolean,
|
||||||
|
default: (ENV["LNDHUB_ADMIN_UI"] || false)
|
||||||
|
|
||||||
|
field :lndhub_public_key, type: :string, readonly: true,
|
||||||
|
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
|
||||||
|
|
||||||
|
field :lndhub_keysend_enabled, type: :boolean,
|
||||||
|
default: -> { self.lndhub_public_key.present?.to_s || false }
|
||||||
|
|
||||||
|
#
|
||||||
|
# Mastodon
|
||||||
|
#
|
||||||
|
|
||||||
|
field :mastodon_public_url, type: :string, readonly: true,
|
||||||
|
default: ENV["MASTODON_PUBLIC_URL"].presence
|
||||||
|
|
||||||
|
field :mastodon_enabled, type: :boolean,
|
||||||
|
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
#
|
||||||
|
# MediaWiki
|
||||||
|
#
|
||||||
|
|
||||||
|
field :mediawiki_public_url, type: :string, readonly: true,
|
||||||
|
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
|
||||||
|
|
||||||
|
field :mediawiki_enabled, type: :boolean,
|
||||||
|
default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Nostr
|
||||||
|
#
|
||||||
|
|
||||||
|
field :nostr_enabled, type: :boolean, default: true
|
||||||
|
end
|
||||||
@@ -3,28 +3,50 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
# 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 :inviter, through: :invitation, source: :user
|
||||||
|
has_many :invitees, through: :invitations
|
||||||
|
|
||||||
has_many :donations, dependent: :nullify
|
has_many :donations, dependent: :nullify
|
||||||
|
|
||||||
|
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
|
||||||
|
primary_key: "ln_account", foreign_key: "login"
|
||||||
|
|
||||||
|
has_many :accounts, through: :lndhub_user
|
||||||
|
|
||||||
validates_uniqueness_of :cn
|
validates_uniqueness_of :cn
|
||||||
validates_length_of :cn, :minimum => 3
|
validates_length_of :cn, :minimum => 3
|
||||||
|
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
||||||
|
if: Proc.new{ |u| u.cn.present? },
|
||||||
|
message: "is invalid. Please use only letters, numbers and -"
|
||||||
|
validates_format_of :cn, without: /\A-/,
|
||||||
|
if: Proc.new{ |u| u.cn.present? },
|
||||||
|
message: "is invalid. Usernames need to start with a letter."
|
||||||
|
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
|
||||||
|
message: "has already been taken"
|
||||||
|
|
||||||
validates_uniqueness_of :email
|
validates_uniqueness_of :email
|
||||||
validates :email, email: true
|
validates :email, email: true
|
||||||
|
|
||||||
lockbox_encrypts :ln_login
|
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||||
lockbox_encrypts :ln_password
|
scope :pending, -> { where(confirmed_at: nil) }
|
||||||
|
|
||||||
|
has_encrypted :ln_login, :ln_password
|
||||||
|
|
||||||
# Include default devise modules. Others available are:
|
# Include default devise modules. Others available are:
|
||||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||||
devise :ldap_authenticatable,
|
devise :ldap_authenticatable,
|
||||||
:confirmable,
|
:confirmable,
|
||||||
:recoverable,
|
:recoverable,
|
||||||
:validatable
|
:validatable,
|
||||||
|
:timeoutable,
|
||||||
|
:rememberable
|
||||||
|
|
||||||
def ldap_before_save
|
def ldap_before_save
|
||||||
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
|
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
|
||||||
|
self.ou = dn.split(',')
|
||||||
dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn")
|
.select{|e| e[0..1] == "ou"}.first
|
||||||
self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=")
|
.delete_prefix("ou=")
|
||||||
|
|
||||||
if self.confirmed_at.blank? && self.confirmation_token.blank?
|
if self.confirmed_at.blank? && self.confirmation_token.blank?
|
||||||
# User had an account with a trusted email address before akkounts was a thing
|
# User had an account with a trusted email address before akkounts was a thing
|
||||||
@@ -32,11 +54,28 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_password(new_password, new_password_confirmation)
|
def devise_after_confirmation
|
||||||
if new_password == new_password_confirmation && ::Devise.ldap_update_password
|
enable_service %w[ discourse ejabberd gitea mediawiki ]
|
||||||
Devise::LDAP::Adapter.update_password(login_with, new_password)
|
|
||||||
|
#TODO enable in development when we have easy setup of ejabberd etc.
|
||||||
|
return if Rails.env.development?
|
||||||
|
|
||||||
|
if inviter.present?
|
||||||
|
exchange_xmpp_contact_with_inviter if Setting.ejabberd_enabled?
|
||||||
end
|
end
|
||||||
clear_reset_password_token if valid?
|
end
|
||||||
|
|
||||||
|
def send_devise_notification(notification, *args)
|
||||||
|
devise_mailer.send(notification, self, *args).deliver_later
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_password(new_password, new_password_confirmation)
|
||||||
|
self.password = new_password
|
||||||
|
self.password_confirmation = new_password_confirmation
|
||||||
|
return false unless valid?
|
||||||
|
|
||||||
|
Devise::LDAP::Adapter.update_password(login_with, new_password)
|
||||||
|
clear_reset_password_token
|
||||||
save
|
save
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -62,4 +101,48 @@ class User < ApplicationRecord
|
|||||||
lndhub.authenticate self
|
lndhub.authenticate self
|
||||||
lndhub.addinvoice payload
|
lndhub.addinvoice payload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dn
|
||||||
|
return @dn if defined?(@dn)
|
||||||
|
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ldap_entry
|
||||||
|
ldap.fetch_users(uid: self.cn, ou: self.ou).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def services_enabled
|
||||||
|
ldap_entry[:service] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_service(service)
|
||||||
|
current_services = services_enabled
|
||||||
|
new_services = Array(service).map(&:to_s)
|
||||||
|
services = (current_services + new_services).uniq
|
||||||
|
ldap.replace_attribute(dn, :service, services)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_service(service)
|
||||||
|
current_services = services_enabled
|
||||||
|
disabled_services = Array(service).map(&:to_s)
|
||||||
|
services = (current_services - disabled_services).uniq
|
||||||
|
ldap.replace_attribute(dn, :service, services)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_all_services
|
||||||
|
ldap.delete_attribute(dn,:service)
|
||||||
|
end
|
||||||
|
|
||||||
|
def exchange_xmpp_contact_with_inviter
|
||||||
|
return unless inviter.services_enabled.include?("ejabberd") &&
|
||||||
|
services_enabled.include?("ejabberd")
|
||||||
|
XmppExchangeContactsJob.perform_later(inviter, self.cn, self.ou)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ldap
|
||||||
|
return @ldap_service if defined?(@ldap_service)
|
||||||
|
@ldap_service = LdapService.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ class CreateAccount < ApplicationService
|
|||||||
def call
|
def call
|
||||||
user = create_user_in_database
|
user = create_user_in_database
|
||||||
add_ldap_document
|
add_ldap_document
|
||||||
create_lndhub_account(user)
|
create_lndhub_account(user) if Setting.lndhub_enabled
|
||||||
|
|
||||||
if @invitation.present?
|
if @invitation.present?
|
||||||
update_invitation(user.id)
|
update_invitation(user.id)
|
||||||
exchange_xmpp_contacts
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,12 +42,6 @@ class CreateAccount < ApplicationService
|
|||||||
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
|
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
|
||||||
end
|
end
|
||||||
|
|
||||||
def exchange_xmpp_contacts
|
|
||||||
#TODO enable in development when we have easy setup of ejabberd etc.
|
|
||||||
return if Rails.env.development?
|
|
||||||
ExchangeXmppContactsJob.perform_later(@invitation.user, @username, @domain)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_lndhub_account(user)
|
def create_lndhub_account(user)
|
||||||
#TODO enable in development when we have a local lndhub (mock?) API
|
#TODO enable in development when we have a local lndhub (mock?) API
|
||||||
return if Rails.env.development?
|
return if Rails.env.development?
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ class EjabberdApiClient
|
|||||||
def add_rosteritem(payload)
|
def add_rosteritem(payload)
|
||||||
post "add_rosteritem", payload
|
post "add_rosteritem", payload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_message(payload)
|
||||||
|
post "send_message", payload
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,18 @@ class LdapService < ApplicationService
|
|||||||
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_attribute(dn, attr, values)
|
||||||
|
ldap_client.add_attribute dn, attr, values
|
||||||
|
end
|
||||||
|
|
||||||
|
def replace_attribute(dn, attr, values)
|
||||||
|
ldap_client.replace_attribute dn, attr, values
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_attribute(dn, attr)
|
||||||
|
ldap_client.delete_attribute dn, attr
|
||||||
|
end
|
||||||
|
|
||||||
def add_entry(dn, attrs, interactive=false)
|
def add_entry(dn, attrs, interactive=false)
|
||||||
puts "Adding entry: #{dn}" if interactive
|
puts "Adding entry: #{dn}" if interactive
|
||||||
res = ldap_client.add dn: dn, attributes: attrs
|
res = ldap_client.add dn: dn, attributes: attrs
|
||||||
@@ -10,10 +22,6 @@ class LdapService < ApplicationService
|
|||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_attribute(dn, attr, value)
|
|
||||||
ldap_client.add_attribute dn, attr, value
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_entry(dn, interactive=false)
|
def delete_entry(dn, interactive=false)
|
||||||
puts "Deleting entry: #{dn}" if interactive
|
puts "Deleting entry: #{dn}" if interactive
|
||||||
res = ldap_client.delete dn: dn
|
res = ldap_client.delete dn: dn
|
||||||
@@ -42,18 +50,17 @@ class LdapService < ApplicationService
|
|||||||
treebase = ldap_config["base"]
|
treebase = ldap_config["base"]
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes = %w{dn cn uid mail admin}
|
attributes = %w{dn cn uid mail admin service}
|
||||||
filter = Net::LDAP::Filter.eq("uid", "*")
|
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] }
|
||||||
|
|
||||||
entries = entries.collect do |e|
|
entries = entries.collect do |e|
|
||||||
{
|
{
|
||||||
uid: e.uid.first,
|
uid: e.uid.first,
|
||||||
mail: e.try(:mail) ? e.mail.first : nil,
|
mail: e.try(:mail) ? e.mail.first : nil,
|
||||||
admin: e.try(:admin) ? 'admin' : nil
|
admin: e.try(:admin) ? 'admin' : nil,
|
||||||
# password: e.userpassword.first
|
service: e.try(:service)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -131,5 +138,4 @@ class LdapService < ApplicationService
|
|||||||
def ldap_config
|
def ldap_config
|
||||||
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class Lndhub
|
|||||||
attr_accessor :auth_token
|
attr_accessor :auth_token
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@base_url = ENV["LNDHUB_LEGACY_API_URL"]
|
@base_url = ENV["LNDHUB_API_URL"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def post(endpoint, payload)
|
def post(endpoint, payload)
|
||||||
@@ -42,7 +42,7 @@ class Lndhub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate(user)
|
def authenticate(user)
|
||||||
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
|
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
|
||||||
self.auth_token = credentials["access_token"]
|
self.auth_token = credentials["access_token"]
|
||||||
self.auth_token
|
self.auth_token
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class LndhubV2
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate(user)
|
def authenticate(user)
|
||||||
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
|
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
|
||||||
self.auth_token = credentials["access_token"]
|
self.auth_token = credentials["access_token"]
|
||||||
self.auth_token
|
self.auth_token
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<%= render HeaderComponent.new(title: "Admin Panel") %>
|
<%= render HeaderComponent.new(title: "Admin Panel") %>
|
||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<p class="text-center">
|
<div class="text-center">
|
||||||
With great power comes great responsibility.
|
<p class="my-12 inline-flex align-center items-center">
|
||||||
</p>
|
<%= image_tag("/img/illustrations/undraw_vault_re_s4my.svg", class: 'h-48') %>
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
With great power comes great responsibility.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,58 +1,41 @@
|
|||||||
<%= form_with(url: url, model: donation, local: true) do |form| %>
|
<%= form_with(url: url, model: donation, local: true) do |form| %>
|
||||||
<% if donation.errors.any? %>
|
<% if donation.errors.any? %>
|
||||||
<div id="error_explanation">
|
<section id="error_explanation">
|
||||||
<h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3>
|
<h3><%= pluralize(donation.errors.count, "error") %> prohibited this donation from being saved:</h3>
|
||||||
<ul>
|
<ul class="list-disc list-inside">
|
||||||
<% donation.errors.full_messages.each do |message| %>
|
<% donation.errors.full_messages.each do |message| %>
|
||||||
<li><%= message %></li>
|
<li><%= message %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="field">
|
<section class="sm:w-1/2 grid grid-cols-2 items-center gap-y-2">
|
||||||
<p>
|
<%= form.label :user_id %>
|
||||||
<%= form.label :user_id %>
|
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn, {} %>
|
||||||
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<%= form.label :amount_sats, "Amount BTC (sats)" %>
|
||||||
<p>
|
<%= form.number_field :amount_sats %>
|
||||||
<%= form.label :amount_sats, "Amount BTC (sats)" %>
|
|
||||||
<%= form.number_field :amount_sats %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
||||||
<p>
|
<%= form.number_field :amount_eur %>
|
||||||
<%= form.label :amount_eur, "Amount EUR (cents)" %>
|
|
||||||
<%= form.number_field :amount_eur %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
||||||
<p>
|
<%= form.number_field :amount_usd %>
|
||||||
<%= form.label :amount_usd, "Amount USD (cents)"%>
|
|
||||||
<%= form.number_field :amount_usd %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<%= form.label :public_name %>
|
||||||
<p>
|
<%= form.text_field :public_name %>
|
||||||
<%= form.label :public_name %>
|
|
||||||
<%= form.text_field :public_name %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
<%= form.label :paid_at %>
|
||||||
<p>
|
<%= form.text_field :paid_at %>
|
||||||
<%= form.label :paid_at %>
|
</section>
|
||||||
<%= form.text_field :paid_at %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="mt-8">
|
<section>
|
||||||
<%= form.submit class: 'btn-md btn-blue' %>
|
<p class="pt-6 border-t border-gray-200 text-right">
|
||||||
</p>
|
<%= link_to 'Cancel',
|
||||||
|
@donation.id.present? ? admin_donation_path(@donation) : admin_donations_path,
|
||||||
|
class: 'btn-md btn-gray' %>
|
||||||
|
<%= form.submit class: 'ml-2 btn-md btn-blue' %>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
<%= render HeaderComponent.new(title: "Donations") %>
|
<%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
|
||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<h2>Editing Donation</h2>
|
|
||||||
|
|
||||||
<%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>
|
<%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>
|
||||||
|
|
||||||
<p class="mt-8">
|
|
||||||
<%= link_to 'Show', admin_donation_path(@donation), class: 'ks-text-link' %> |
|
|
||||||
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
|
|
||||||
<p>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
<%= render HeaderComponent.new(title: "Donations") %>
|
<%= render HeaderComponent.new(title: "Donations") %>
|
||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<%= render QuickstatsContainerComponent.new do %>
|
||||||
|
<%= render QuickstatsItemComponent.new(
|
||||||
|
type: :number,
|
||||||
|
title: 'Overall',
|
||||||
|
value: @stats[:overall_sats],
|
||||||
|
unit: 'sats'
|
||||||
|
) %>
|
||||||
|
<%= render QuickstatsItemComponent.new(
|
||||||
|
type: :number,
|
||||||
|
title: 'Donors',
|
||||||
|
value: @stats[:donor_count],
|
||||||
|
meta: "/ #{User.count} users"
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
<% if @donations.any? %>
|
<% if @donations.any? %>
|
||||||
<table>
|
<h3>Recent Donations</h3>
|
||||||
|
<table class="divided mb-8">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
@@ -11,32 +30,35 @@
|
|||||||
<th class="text-right">in USD</th>
|
<th class="text-right">in USD</th>
|
||||||
<th class="pl-2">Public name</th>
|
<th class="pl-2">Public name</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th colspan="3"></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<% @donations.each do |donation| %>
|
<% @donations.each do |donation| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= donation.user.address %></td>
|
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
|
||||||
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
|
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
|
||||||
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
|
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
|
||||||
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
|
||||||
<td class="pl-2"><%= donation.public_name %></td>
|
<td class="pl-2"><%= donation.public_name %></td>
|
||||||
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d") : "" %></td>
|
<td><%= donation.paid_at ? donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") : "" %></td>
|
||||||
<td><%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
|
<td class="text-right">
|
||||||
<td><%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %></td>
|
<%= link_to 'Show', admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
|
||||||
<td><%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
|
<%= link_to 'Edit', edit_admin_donation_path(donation), class: 'btn btn-sm btn-gray' %>
|
||||||
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %></td>
|
<%= link_to 'Destroy', admin_donation_path(donation), class: 'btn btn-sm btn-red',
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<%== pagy_nav @pagy %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>
|
<p>
|
||||||
No donations yet.
|
No donations yet.
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
<p class="mt-12">
|
<p class="mt-12">
|
||||||
<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %>
|
<%= link_to 'Record an out-of-system donation', new_admin_donation_path, class: 'btn-md btn-gray' %>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<%= render HeaderComponent.new(title: "Donations") %>
|
<%= render HeaderComponent.new(title: "Add Donation") %>
|
||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<h2>New Donation</h2>
|
|
||||||
|
|
||||||
<%= render 'form', donation: @donation, url: admin_donations_path %>
|
<%= render 'form', donation: @donation, url: admin_donations_path %>
|
||||||
|
|
||||||
<p class="mt-8">
|
|
||||||
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
<%= render HeaderComponent.new(title: "Donations") %>
|
<%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
|
||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<p>
|
<section>
|
||||||
<strong>User:</strong>
|
<table class="w-1/2 divided">
|
||||||
<%= @donation.user.address %>
|
<tbody>
|
||||||
</p>
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Amount sats</th>
|
||||||
|
<td><%= @donation.amount_sats %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Amount EUR</th>
|
||||||
|
<td><%= @donation.amount_eur %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Amount USD</th>
|
||||||
|
<td><%= @donation.amount_usd %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Public name</th>
|
||||||
|
<td><%= @donation.public_name %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
<p>
|
<section>
|
||||||
<strong>Amount sats:</strong>
|
<p class="pt-6 border-t border-gray-200 text-right">
|
||||||
<%= @donation.amount_sats %>
|
<%= link_to 'Back', admin_donations_path, class: 'btn-md btn-gray' %>
|
||||||
</p>
|
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ml-2 btn-md btn-blue mr-1' %>
|
||||||
|
</p>
|
||||||
<p>
|
</section>
|
||||||
<strong>Amount eur:</strong>
|
|
||||||
<%= @donation.amount_eur %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>Amount usd:</strong>
|
|
||||||
<%= @donation.amount_usd %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>Public name:</strong>
|
|
||||||
<%= @donation.public_name %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>Date:</strong>
|
|
||||||
<%= @donation.paid_at %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="mt-8">
|
|
||||||
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> |
|
|
||||||
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -2,17 +2,29 @@
|
|||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<section>
|
<section>
|
||||||
<p>
|
<%= render QuickstatsContainerComponent.new do %>
|
||||||
There are currently <strong><%= @invitations_unused_count %>
|
<%= render QuickstatsItemComponent.new(
|
||||||
unused invitations</strong> available to existing users.
|
type: :number,
|
||||||
<strong><%= @users_with_referrals_count %> users</strong> have successfully
|
title: 'Available',
|
||||||
invited new users.
|
value: @stats[:available],
|
||||||
</p>
|
) %>
|
||||||
|
<%= 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>
|
</section>
|
||||||
<% if @invitations_used.any? %>
|
<% if @invitations_used.any? %>
|
||||||
<section>
|
<section>
|
||||||
<h3>Accepted (<%= @invitations_used.length %>)</h3>
|
<h3>Recently Accepted</h3>
|
||||||
<table>
|
<table class="divided mb-8">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Token</th>
|
<th>Token</th>
|
||||||
@@ -26,12 +38,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
|
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
|
||||||
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||||
<td><%= invitation.user.address %></td>
|
<td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
|
||||||
<td><%= User.find(invitation.invited_user_id).address %></td>
|
<td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<%== pagy_nav @pagy %>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %>
|
|
||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
|
||||||
<h3 class="hidden">Domains</h3>
|
|
||||||
<ul class="mb-10">
|
|
||||||
<li class="inline-block">
|
|
||||||
<%= link_to 'kosmos.org', admin_ldap_users_path, class: "ks-text-link" %>
|
|
||||||
</li>
|
|
||||||
<li class="inline-block ml-6">
|
|
||||||
<%= link_to '5apps.com', admin_ldap_users_path(ou: '5apps.com'), class: "ks-text-link" %>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>UID</th>
|
|
||||||
<th>E-Mail</th>
|
|
||||||
<th>Admin</th>
|
|
||||||
<!-- <th>Password</th> -->
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<% @entries.each do |entry| %>
|
|
||||||
<tr>
|
|
||||||
<td><%= entry[:uid] %></td>
|
|
||||||
<td><%= entry[:mail] %></td>
|
|
||||||
<td><%= entry[:admin] %></td>
|
|
||||||
<!-- <td><%= entry[:password] %></td> -->
|
|
||||||
</tr>
|
|
||||||
<% end %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<% end %>
|
|
||||||
48
app/views/admin/lightning/index.html.erb
Normal file
48
app/views/admin/lightning/index.html.erb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Lightning Network") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<%= render QuickstatsContainerComponent.new do %>
|
||||||
|
<%= render QuickstatsItemComponent.new(
|
||||||
|
type: :number,
|
||||||
|
title: 'Current user balance',
|
||||||
|
value: @ln[:current_balance],
|
||||||
|
unit: 'sats'
|
||||||
|
) %>
|
||||||
|
<%= render QuickstatsItemComponent.new(
|
||||||
|
type: :number,
|
||||||
|
title: 'Users with sats',
|
||||||
|
value: @ln[:users_with_sats],
|
||||||
|
meta: "/ #{User.count}"
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Accounts</h3>
|
||||||
|
<table class="divided">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>LN Account</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Balance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @accounts.each do |account| %>
|
||||||
|
<tr>
|
||||||
|
<td class="font-mono">
|
||||||
|
<%= account.login %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if user = @users.find{ |u| u[2] == account.login } %>
|
||||||
|
<%= link_to "#{user[0]}@#{user[1]}", admin_user_path("#{user[0]}@#{user[1]}"), class: "ks-text-link" %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
7
app/views/admin/settings/_errors.html.erb
Normal file
7
app/views/admin/settings/_errors.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<section>
|
||||||
|
<ul>
|
||||||
|
<% errors.full_messages.each do |msg| %>
|
||||||
|
<li><%= msg %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
32
app/views/admin/settings/registrations/index.html.erb
Normal file
32
app/views/admin/settings/registrations/index.html.erb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Settings") %>
|
||||||
|
|
||||||
|
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||||
|
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
|
||||||
|
<section>
|
||||||
|
<h3>Registrations</h3>
|
||||||
|
|
||||||
|
<% if @errors && @errors.any? %>
|
||||||
|
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
<p class="font-bold mb-1">Reserved usernames</p>
|
||||||
|
<p class="text-gray-500">
|
||||||
|
These usernames cannot be registered as accounts:
|
||||||
|
</p>
|
||||||
|
<%= f.text_area :reserved_usernames,
|
||||||
|
value: Setting.reserved_usernames.join("\n"),
|
||||||
|
class: "h-44 mb-2" %>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
One username per line
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p class="pt-6 border-t border-gray-200 text-right">
|
||||||
|
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
17
app/views/admin/settings/services/_discourse.html.erb
Normal file
17
app/views/admin/settings/services/_discourse.html.erb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<h3>Discourse</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :discourse_enabled,
|
||||||
|
enabled: Setting.discourse_enabled?,
|
||||||
|
title: "Enable Discourse integration",
|
||||||
|
description: "Discourse configuration present and features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.discourse_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||||
|
<%= f.text_field :discourse_public_url,
|
||||||
|
value: Setting.discourse_public_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
30
app/views/admin/settings/services/_ejabberd.html.erb
Normal file
30
app/views/admin/settings/services/_ejabberd.html.erb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<h3>ejabberd (XMPP)</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :ejabberd_enabled,
|
||||||
|
enabled: Setting.ejabberd_enabled?,
|
||||||
|
title: "Enable ejabberd integration",
|
||||||
|
description: "ejabberd configuration present and features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.ejabberd_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
|
||||||
|
<%= f.text_field :ejabberd_api_url,
|
||||||
|
value: Setting.ejabberd_api_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Admin URL") do %>
|
||||||
|
<%= f.text_field :ejabberd_admin_url,
|
||||||
|
value: Setting.ejabberd_admin_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
title: "Contact roster name",
|
||||||
|
description: "Used when exchanging contacts after signup from invitation"
|
||||||
|
) do %>
|
||||||
|
<%= f.text_field :ejabberd_buddy_roster,
|
||||||
|
value: Setting.ejabberd_buddy_roster,
|
||||||
|
class: "w-full" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
17
app/views/admin/settings/services/_gitea.html.erb
Normal file
17
app/views/admin/settings/services/_gitea.html.erb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<h3>Gitea</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :gitea_enabled,
|
||||||
|
enabled: Setting.gitea_enabled?,
|
||||||
|
title: "Enable Gitea integration",
|
||||||
|
description: "Gitea configuration present and features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.gitea_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||||
|
<%= f.text_field :gitea_public_url,
|
||||||
|
value: Setting.gitea_public_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
38
app/views/admin/settings/services/_lndhub.html.erb
Normal file
38
app/views/admin/settings/services/_lndhub.html.erb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<h3>Lightning Network</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :lndhub_enabled,
|
||||||
|
enabled: Setting.lndhub_enabled?,
|
||||||
|
title: "Enable LNDHub integration",
|
||||||
|
description: "LNDHub configuration present and wallet features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.lndhub_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
|
||||||
|
<%= f.text_field :lndhub_api_url,
|
||||||
|
value: Setting.lndhub_api_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :lndhub_admin_enabled,
|
||||||
|
enabled: Setting.lndhub_admin_enabled?,
|
||||||
|
title: "Enable LNDHub admin panel",
|
||||||
|
description: "LNDHub database configuration present and admin panel enabled"
|
||||||
|
) %>
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :lndhub_keysend_enabled,
|
||||||
|
enabled: Setting.lndhub_keysend_enabled?,
|
||||||
|
title: "Enable keysend payments",
|
||||||
|
description: "Allow users to receive invoice-less payments to their Lightning Address"
|
||||||
|
) %>
|
||||||
|
<% if Setting.lndhub_keysend_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Public key", description: "The public key of the Lightning node used by LNDHub") do %>
|
||||||
|
<%= f.text_field :lndhub_public_key,
|
||||||
|
value: Setting.lndhub_public_key,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
17
app/views/admin/settings/services/_mastodon.html.erb
Normal file
17
app/views/admin/settings/services/_mastodon.html.erb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<h3>Mastodon</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :mastodon_enabled,
|
||||||
|
enabled: Setting.mastodon_enabled?,
|
||||||
|
title: "Enable Mastodon integration",
|
||||||
|
description: "Mastodon configuration present and features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.mastodon_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||||
|
<%= f.text_field :mastodon_public_url,
|
||||||
|
value: Setting.mastodon_public_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
17
app/views/admin/settings/services/_mediawiki.html.erb
Normal file
17
app/views/admin/settings/services/_mediawiki.html.erb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<h3>MediaWiki</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :mediawiki_enabled,
|
||||||
|
enabled: Setting.mediawiki_enabled?,
|
||||||
|
title: "Enable MediaWiki integration",
|
||||||
|
description: "MediaWiki configuration present and features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.mediawiki_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||||
|
<%= f.text_field :mediawiki_public_url,
|
||||||
|
value: Setting.mediawiki_public_url,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
10
app/views/admin/settings/services/_nostr.html.erb
Normal file
10
app/views/admin/settings/services/_nostr.html.erb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<h3>Nostr</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :nostr_enabled,
|
||||||
|
enabled: Setting.nostr_enabled?,
|
||||||
|
title: "Enable Nostr integration (experimental)",
|
||||||
|
description: "Allow adding nostr pubkeys and resolve user addresses via NIP-05"
|
||||||
|
) %>
|
||||||
|
</ul>
|
||||||
23
app/views/admin/settings/services/index.html.erb
Normal file
23
app/views/admin/settings/services/index.html.erb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Settings") %>
|
||||||
|
|
||||||
|
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||||
|
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
|
||||||
|
<%= hidden_field_tag :service, @service %>
|
||||||
|
|
||||||
|
<% if @errors && @errors.any? %>
|
||||||
|
<section>
|
||||||
|
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<%= render partial: @service, locals: { f: f } %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p class="pt-6 border-t border-gray-200 text-right">
|
||||||
|
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
54
app/views/admin/users/index.html.erb
Normal file
54
app/views/admin/users/index.html.erb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Users: #{@ou}") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<%= render QuickstatsContainerComponent.new do %>
|
||||||
|
<%= render QuickstatsItemComponent.new(
|
||||||
|
type: :number,
|
||||||
|
title: 'Confirmed',
|
||||||
|
value: @stats[:users_confirmed],
|
||||||
|
) %>
|
||||||
|
<%= render QuickstatsItemComponent.new(
|
||||||
|
type: :number,
|
||||||
|
title: 'Pending',
|
||||||
|
value: @stats[:users_pending],
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<% if @orgs.length > 1 %>
|
||||||
|
<section>
|
||||||
|
<h3 class="hidden">Domains</h3>
|
||||||
|
<ul>
|
||||||
|
<% @orgs.each do |org| %>
|
||||||
|
<li class="inline-block">
|
||||||
|
<%= link_to org[:ou], admin_users_path(ou: org[:ou]), class: "ks-text-link" %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<table class="divided mb-8">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>UID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Roles</th>
|
||||||
|
<!-- <th>Password</th> -->
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @users.each do |user| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %></td>
|
||||||
|
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
|
||||||
|
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<%== pagy_nav @pagy %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
182
app/views/admin/users/show.html.erb
Normal file
182
app/views/admin/users/show.html.erb
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "User: #{@user.address}") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
|
||||||
|
<section class="sm:flex-1">
|
||||||
|
<h3>Account</h3>
|
||||||
|
<table class="divided">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Created at</th>
|
||||||
|
<td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Confirmed at</th>
|
||||||
|
<td>
|
||||||
|
<% if @user.confirmed_at %>
|
||||||
|
<%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
|
||||||
|
<% else %>
|
||||||
|
<%= badge "pending", :yellow %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<td><%= @user.email %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Roles</th>
|
||||||
|
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Invited by</th>
|
||||||
|
<td>
|
||||||
|
<% if @user.inviter %>
|
||||||
|
<%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
|
||||||
|
<% else %>—<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Invitations available</th>
|
||||||
|
<td>
|
||||||
|
<%= @user.invitations.count %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="align-top">Invited users</th>
|
||||||
|
<td class="align-top">
|
||||||
|
<% if @user.invitees.length > 0 %>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<% @user.invitees.order(cn: :asc).each do |invitee| %>
|
||||||
|
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% else %>—<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="sm:flex-1 sm:pt-0">
|
||||||
|
<!-- <h3>Actions</h3> -->
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Services</h3>
|
||||||
|
<table class="divided">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% if Setting.discourse_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>Discourse</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @services_enabled.include?("discourse"),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<%= link_to "Open profile", "#{Setting.discourse_public_url}/u/#{@user.cn}/summary", class: "btn-sm btn-gray" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.gitea_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>Gitea</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @services_enabled.include?("gitea"),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<%= link_to "Open profile", "#{Setting.gitea_public_url}/#{@user.cn}", class: "btn-sm btn-gray" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.mastodon_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>Mastodon</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @services_enabled.include?("mastodon"),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<%= link_to "Open profile", "#{Setting.mastodon_public_url}/@#{@user.cn}", class: "btn-sm btn-gray" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.mediawiki_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>MediaWiki</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @services_enabled.include?("mediawiki"),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<%= link_to "Open profile", "#{Setting.mediawiki_public_url}/Special:Contributions/#{@user.cn}", class: "btn-sm btn-gray" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.ejabberd_enabled %>
|
||||||
|
<tr>
|
||||||
|
<td>XMPP (ejabberd)</td>
|
||||||
|
<td>
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: @services_enabled.include?("ejabberd"),
|
||||||
|
input_enabled: false
|
||||||
|
) %>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<% if Setting.ejabberd_admin_url.present? %>
|
||||||
|
<%= link_to "Open profile", "#{Setting.ejabberd_admin_url}/server/#{@user.ou}/user/#{@user.cn}/", class: "btn-sm btn-gray" %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<% if Setting.lndhub_admin_enabled? && @user.confirmed? %>
|
||||||
|
<section>
|
||||||
|
<h3>LndHub</h3>
|
||||||
|
<% if @lndhub_user %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Account</th>
|
||||||
|
<th>Balance</th>
|
||||||
|
<th>Incoming</th>
|
||||||
|
<th>Outgoing</th>
|
||||||
|
<th>Fees</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><%= @user.ln_account %></td>
|
||||||
|
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
|
||||||
|
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
|
||||||
|
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
|
||||||
|
<td><%= number_with_delimiter @lndhub_user.sum_fees %> sats</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
1
app/views/custom_mailer/custom_message.text.erb
Normal file
1
app/views/custom_mailer/custom_message.text.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<%= @body %>
|
||||||
@@ -2,60 +2,87 @@
|
|||||||
|
|
||||||
<%= render MainSimpleComponent.new do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<section>
|
<section>
|
||||||
<p>
|
<p class="mb-8">
|
||||||
Your Kosmos account and password currently give you access to these
|
Your Kosmos account and password currently give you access to these
|
||||||
services:
|
services:
|
||||||
</p>
|
</p>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 services mt-12">
|
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||||
<div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">
|
bg-cover bg-[center_top_-50px] bg-no-repeat
|
||||||
<%= link_to "Chat", "https://wiki.kosmos.org/Services:Chat", class: "ks-text-link" %>
|
bg-[url(/img/logos/icon_xmpp.svg)]">
|
||||||
</h3>
|
<%= link_to "https://wiki.kosmos.org/Services:Chat",
|
||||||
<p class="text-gray-500">
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
Chat rooms and instant messaging (XMPP/Jabber)
|
<h3 class="mb-3.5">Chat</h3>
|
||||||
</p>
|
<p class="text-gray-600">
|
||||||
|
Federated chat rooms and instant messaging
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">
|
bg-[length:95%] bg-center bg-no-repeat
|
||||||
<%= link_to "Discourse", "https://community.kosmos.org", class: "ks-text-link" %>
|
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||||
</h3>
|
<%= link_to "https://community.kosmos.org",
|
||||||
<p class="text-gray-500">
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
Kosmos community forums and user support/help site
|
<h3 class="mb-3.5">Discourse</h3>
|
||||||
</p>
|
<p class="text-gray-600">
|
||||||
|
Kosmos community forums and user support/help site
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">
|
bg-cover bg-[center_top_-20px] bg-no-repeat
|
||||||
<%= render partial: "icons/zap", locals: { custom_class: "text-amber-500 h-4 w-4 inline" } %>
|
bg-[url(/img/logos/icon_mediawiki.svg)]">
|
||||||
<%= link_to "Lightning Wallet", wallet_path, class: "ks-text-link" %>
|
<%= link_to "https://wiki.kosmos.org",
|
||||||
</h3>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<p class="text-gray-500">
|
<h3 class="mb-3.5">Wiki</h3>
|
||||||
Send and receive sats over the Bitcoin Lightning Network
|
<p class="text-gray-600">
|
||||||
</p>
|
Kosmos documentation and knowledge base
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">
|
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
|
||||||
<%= link_to "Wiki", "https://wiki.kosmos.org", class: "ks-text-link" %>
|
bg-[url(/img/logos/icon_lightning.svg)]">
|
||||||
</h3>
|
<%= link_to wallet_path,
|
||||||
<p class="text-gray-500">
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
Kosmos documentation and knowledge base
|
<h3 class="mb-3.5">Wallet</h3>
|
||||||
</p>
|
<p class="text-gray-600">
|
||||||
|
Send and receive sats over the Bitcoin Lightning Network
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">
|
bg-cover bg-center bg-no-repeat
|
||||||
<%= link_to "Gitea", "https://gitea.kosmos.org", class: "ks-text-link" %>
|
bg-[url(/img/logos/icon_gitea.png)]">
|
||||||
</h3>
|
<%= link_to "https://gitea.kosmos.org",
|
||||||
<p class="text-gray-500">
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
Code hosting and collaboration for software projects
|
<h3 class="mb-3.5">Gitea</h3>
|
||||||
</p>
|
<p class="text-gray-600">
|
||||||
|
Code hosting and collaboration for software projects
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">
|
bg-cover bg-[center_top_-70px] bg-no-repeat
|
||||||
<%= link_to "Drone CI", "https://drone.kosmos.org", class: "ks-text-link" %>
|
bg-[url(/img/logos/icon_droneci.svg)]">
|
||||||
</h3>
|
<%= link_to "https://drone.kosmos.org",
|
||||||
<p class="text-gray-500">
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
Continuous integration for software projects on Gitea
|
<h3 class="mb-3.5">Drone CI</h3>
|
||||||
</p>
|
<p class="text-gray-600">
|
||||||
|
Continuous integration for software projects on Gitea
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="border border-gray-300 rounded-md hover:border-gray-400 -->
|
||||||
|
<!-- bg-[length:80%] bg-[right_top_-30px] bg-no-repeat -->
|
||||||
|
<!-- bg-[url(/img/logos/icon_mastodon.svg)]"> -->
|
||||||
|
<!-- <%= link_to "https://kosmos.social", class: "block h-full px-6 py-6 rounded-md" do %> -->
|
||||||
|
<!-- <h3 class="mb-3.5">Mastodon</h3> -->
|
||||||
|
<!-- <p class="text-gray-400"> -->
|
||||||
|
<!-- Your account on the Open Social Web -->
|
||||||
|
<!-- </p> -->
|
||||||
|
<!-- <% end %> -->
|
||||||
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
|
||||||
<%= render "devise/shared/error_messages", resource: resource %>
|
<%= render "devise/shared/error_messages", resource: resource %>
|
||||||
<p>
|
<p>
|
||||||
<%= f.label :email, 'Email address', class: 'block mb-1 w-full' %>
|
<%= f.label :email, 'Email address', class: 'block mb-2 font-bold' %>
|
||||||
<%= f.email_field :email,
|
<%= f.email_field :email,
|
||||||
required: true, autofocus: true, autocomplete: "email",
|
required: true, autofocus: true, autocomplete: "email",
|
||||||
value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
|
value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="mt-8">
|
<p class="mt-8">
|
||||||
<%= f.submit "Resend confirmation link",
|
<%= f.submit "Resend confirmation link",
|
||||||
class: 'btn-md btn-blue w-full sm:w-auto' %>
|
class: 'btn-md btn-blue w-full' %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<%= f.label :password, "New password" %>
|
<%= f.label :password, "New password" %>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
|
<%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "w-full" %>
|
||||||
<% if @minimum_password_length %>
|
<% if @minimum_password_length %>
|
||||||
<br><em class="text-sm text-gray-500">(<%= @minimum_password_length %> characters minimum)</em>
|
<br><em class="text-sm text-gray-500">(<%= @minimum_password_length %> characters minimum)</em>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -20,10 +20,10 @@
|
|||||||
<%= f.label :password_confirmation, "Confirm new password" %>
|
<%= f.label :password_confirmation, "Confirm new password" %>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "w-full" %>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-8">
|
<p class="mt-8">
|
||||||
<%= f.submit "Change my password", class: 'btn-md btn-blue' %>
|
<%= f.submit "Change my password", class: 'btn-md btn-blue w-full' %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,21 @@
|
|||||||
|
|
||||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
||||||
<%= render "devise/shared/error_messages", resource: resource %>
|
<%= render "devise/shared/error_messages", resource: resource %>
|
||||||
|
<div class="mb-6">
|
||||||
|
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
||||||
|
<p class="flex gap-2 items-center">
|
||||||
|
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
||||||
|
required: true, class: "relative grow"%>
|
||||||
|
<span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<%= f.label :cn, 'User', class: 'block' %>
|
<%= f.label :email, 'Email address', class: 'block mb-2 font-bold' %>
|
||||||
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
|
||||||
required: true, class: "w-full md:w-3/5"%>
|
|
||||||
<span class="ml-1 text-gray-500">@ kosmos.org</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<%= f.label :email, 'Email address', class: 'block' %>
|
|
||||||
<%= f.email_field :email, autocomplete: "email", required: true,
|
<%= f.email_field :email, autocomplete: "email", required: true,
|
||||||
class: "w-full md:w-3/5"%>
|
class: "w-full"%>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-8">
|
<p class="mt-8">
|
||||||
<%= f.submit "Send me a reset link", class: 'btn-md btn-blue w-full sm:w-auto' %>
|
<%= f.submit "Send me a reset link", class: 'btn-md btn-blue w-full' %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,47 @@
|
|||||||
<%= render MainCompactComponent.new do %>
|
<%= render MainCompactComponent.new do %>
|
||||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
|
||||||
<%= render "devise/shared/error_messages", resource: resource %>
|
<%= render "devise/shared/error_messages", resource: resource %>
|
||||||
<p>
|
<div class="mb-6">
|
||||||
<%= f.label :cn, 'User', class: 'block' %>
|
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
||||||
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
<p class="flex gap-2 items-center">
|
||||||
class: "w-full md:w-3/5"%>
|
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
||||||
<span class="ml-1 text-gray-500">@ kosmos.org</span>
|
required: true, class: "relative grow", tabindex: "1" %>
|
||||||
</p>
|
<span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
|
||||||
<p>
|
</p>
|
||||||
<%= f.label :password, class: 'block' %>
|
</div>
|
||||||
|
<p class="mb-8">
|
||||||
|
<%= f.label :password, class: 'block mb-2 font-bold' %>
|
||||||
<%= f.password_field :password, autocomplete: "current-password",
|
<%= f.password_field :password, autocomplete: "current-password",
|
||||||
class: "w-full md:w-3/5"%>
|
required: true, class: "w-full", tabindex: "2" %>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-8">
|
|
||||||
<%= f.submit "Log in", class: 'btn-md btn-blue w-full sm:w-auto' %>
|
<%= tag.div class: "flex items-center mb-8 gap-x-3", data: {
|
||||||
|
controller: "settings--toggle",
|
||||||
|
:'settings--toggle-switch-enabled-value' => "false"
|
||||||
|
} do %>
|
||||||
|
<div class="relative inline-flex flex-shrink-0">
|
||||||
|
<%= render FormElements::ToggleComponent.new(
|
||||||
|
enabled: false, input_enabled: true, class_names: "hidden",
|
||||||
|
tabindex: "3", data: {
|
||||||
|
:'settings--toggle-target' => "button",
|
||||||
|
action: "settings--toggle#toggleSwitch"
|
||||||
|
}) %>
|
||||||
|
<%= f.check_box :remember_me, {
|
||||||
|
checked: false,
|
||||||
|
data: { :'settings--toggle-target' => "checkbox" }
|
||||||
|
}, "true", "false" %>
|
||||||
|
</div>
|
||||||
|
<%= f.label :remember_me,
|
||||||
|
class: "text-gray-500 flex flex-col",
|
||||||
|
data: { action: "click->settings--toggle#toggleSwitch" } %>
|
||||||
|
<p class="grow text-sm text-right">
|
||||||
|
<%= link_to "Forgot your password?", new_password_path(resource_name),
|
||||||
|
class: "text-gray-500 underline" %><br />
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= f.submit "Log in", class: 'btn-md btn-blue w-full', tabindex: "4" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= render "devise/shared/links" %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
<div class="devise-links mt-8 text-sm">
|
<div class="devise-links mt-8 text-sm">
|
||||||
<%- if controller_name != 'sessions' %>
|
<%- if controller_name != 'sessions' %>
|
||||||
<p class="mb-1.5">
|
<p class="mb-2">
|
||||||
<%= link_to "Log in", new_session_path(resource_name) %><br />
|
<%= link_to "Log in", new_session_path(resource_name),
|
||||||
|
class: "text-gray-500 underline" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||||
<p class="mb-1.5">
|
<p class="mb-2">
|
||||||
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
|
<%= link_to "Forgot your password?", new_password_path(resource_name),
|
||||||
|
class: "text-gray-500 underline" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
<%- if devise_mapping.confirmable? && !controller_name.match(/^(confirmations|sessions)$/) %>
|
||||||
<p class="mb-1.5">
|
<p class="mb-2">
|
||||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
|
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name),
|
||||||
|
class: "text-gray-500 underline" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||||
<p class="mb-1.5">
|
<p class="mb-2">
|
||||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
|
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name),
|
||||||
|
class: "text-gray-500 underline" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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-grid"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></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-grid <%= custom_class %>"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 425 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"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></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 <%= custom_class %>"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 320 B |
@@ -6,14 +6,13 @@
|
|||||||
<p class="mb-8">
|
<p class="mb-8">
|
||||||
Invite your friends to a Kosmos account by sharing an invitation URL with them:
|
Invite your friends to a Kosmos account by sharing an invitation URL with them:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul class="md:w-3/4">
|
||||||
<% @invitations_unused.each do |invitation| %>
|
<% @invitations_unused.each do |invitation| %>
|
||||||
<li class="font-mono mb-1 flex gap-1 md:block"
|
<li class="font-mono mb-2 flex gap-1" data-controller="clipboard">
|
||||||
data-controller="clipboard">
|
<input type="text" disabled class="relative grow"
|
||||||
<input type="text" disabled class="md:w-3/4 flex-1"
|
|
||||||
value="<%= invitation_url(invitation.token) %>"
|
value="<%= invitation_url(invitation.token) %>"
|
||||||
data-clipboard-target="source" />
|
data-clipboard-target="source" />
|
||||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue flex-none w-auto"
|
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0 w-auto"
|
||||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
title="Copy to clipboard">
|
title="Copy to clipboard">
|
||||||
<span class="content-initial">
|
<span class="content-initial">
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
<style>
|
<style></style>
|
||||||
/* Email styles need to be inline */
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user