Merge branch 'master' into feature/rs-oauth
This commit is contained in:
commit
eeabbdb7df
@ -1,3 +1,5 @@
|
|||||||
|
AKKOUNTS_DOMAIN=accounts.example.com
|
||||||
|
|
||||||
SMTP_SERVER=smtp.example.com
|
SMTP_SERVER=smtp.example.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_LOGIN=accounts
|
SMTP_LOGIN=accounts
|
||||||
@ -7,6 +9,8 @@ SMTP_DOMAIN=example.com
|
|||||||
SMTP_AUTH_METHOD=plain
|
SMTP_AUTH_METHOD=plain
|
||||||
SMTP_ENABLE_STARTTLS=auto
|
SMTP_ENABLE_STARTTLS=auto
|
||||||
|
|
||||||
|
REDIS_URL='redis://localhost:6379/1'
|
||||||
|
|
||||||
LDAP_HOST=localhost
|
LDAP_HOST=localhost
|
||||||
LDAP_PORT=389
|
LDAP_PORT=389
|
||||||
LDAP_ADMIN_PASSWORD=passthebutter
|
LDAP_ADMIN_PASSWORD=passthebutter
|
||||||
|
@ -6,4 +6,6 @@ LNDHUB_API_URL='http://localhost:3026'
|
|||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||||
|
|
||||||
|
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||||
|
|
||||||
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
|
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"]
|
|
||||||
|
5
Gemfile
5
Gemfile
@ -48,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'
|
||||||
@ -62,6 +66,7 @@ group :development do
|
|||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
gem 'letter_opener_web'
|
gem 'letter_opener_web'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
gem 'solargraph'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
56
Gemfile.lock
56
Gemfile.lock
@ -68,7 +68,10 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.1)
|
addressable (2.8.1)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
ast (2.4.2)
|
||||||
|
backport (1.2.0)
|
||||||
bcrypt (3.1.18)
|
bcrypt (3.1.18)
|
||||||
|
benchmark (0.2.1)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
@ -109,6 +112,7 @@ GEM
|
|||||||
dotenv-rails (2.8.1)
|
dotenv-rails (2.8.1)
|
||||||
dotenv (= 2.8.1)
|
dotenv (= 2.8.1)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
|
e2mmap (0.1.0)
|
||||||
erubi (1.11.0)
|
erubi (1.11.0)
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
@ -135,9 +139,15 @@ GEM
|
|||||||
importmap-rails (1.1.5)
|
importmap-rails (1.1.5)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
jaro_winkler (1.5.4)
|
||||||
jbuilder (2.11.5)
|
jbuilder (2.11.5)
|
||||||
actionview (>= 5.0.0)
|
actionview (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
|
json (2.6.3)
|
||||||
|
kramdown (2.4.0)
|
||||||
|
rexml
|
||||||
|
kramdown-parser-gfm (1.1.0)
|
||||||
|
kramdown (~> 2.0)
|
||||||
launchy (2.5.0)
|
launchy (2.5.0)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
@ -179,6 +189,9 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
pagy (6.0.2)
|
pagy (6.0.2)
|
||||||
|
parallel (1.22.1)
|
||||||
|
parser (3.2.1.1)
|
||||||
|
ast (~> 2.4.1)
|
||||||
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)
|
||||||
@ -217,6 +230,7 @@ GEM
|
|||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
@ -229,6 +243,8 @@ GEM
|
|||||||
responders (3.1.0)
|
responders (3.1.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
|
reverse_markdown (2.1.1)
|
||||||
|
nokogiri
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rqrcode (2.1.2)
|
rqrcode (2.1.2)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@ -251,9 +267,27 @@ GEM
|
|||||||
rspec-mocks (~> 3.11)
|
rspec-mocks (~> 3.11)
|
||||||
rspec-support (~> 3.11)
|
rspec-support (~> 3.11)
|
||||||
rspec-support (3.12.0)
|
rspec-support (3.12.0)
|
||||||
|
rubocop (1.48.1)
|
||||||
|
json (~> 2.3)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.2.0.0)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
|
rexml (>= 3.2.5, < 4.0)
|
||||||
|
rubocop-ast (>= 1.26.0, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
|
rubocop-ast (1.28.0)
|
||||||
|
parser (>= 3.2.1.0)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
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)
|
||||||
@ -263,6 +297,21 @@ GEM
|
|||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 4, < 7)
|
sidekiq (>= 4, < 7)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
|
solargraph (0.48.0)
|
||||||
|
backport (~> 1.2)
|
||||||
|
benchmark
|
||||||
|
bundler (>= 1.17.2)
|
||||||
|
diff-lcs (~> 1.4)
|
||||||
|
e2mmap
|
||||||
|
jaro_winkler (~> 1.5)
|
||||||
|
kramdown (~> 2.3)
|
||||||
|
kramdown-parser-gfm (~> 1.1)
|
||||||
|
parser (~> 3.0)
|
||||||
|
reverse_markdown (>= 1.0.5, < 3)
|
||||||
|
rubocop (>= 0.52)
|
||||||
|
thor (~> 1.0)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
yard (~> 0.9, >= 0.9.24)
|
||||||
sprockets (4.1.1)
|
sprockets (4.1.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
@ -284,6 +333,7 @@ GEM
|
|||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
tzinfo (2.0.5)
|
tzinfo (2.0.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
unicode-display_width (2.4.2)
|
||||||
view_component (2.78.0)
|
view_component (2.78.0)
|
||||||
activesupport (>= 5.0.0, < 8.0)
|
activesupport (>= 5.0.0, < 8.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
@ -299,11 +349,14 @@ GEM
|
|||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
webrick (1.7.0)
|
||||||
websocket-driver (0.7.5)
|
websocket-driver (0.7.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
yard (0.9.28)
|
||||||
|
webrick (~> 1.7.0)
|
||||||
zeitwerk (2.6.6)
|
zeitwerk (2.6.6)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
@ -335,8 +388,11 @@ DEPENDENCIES
|
|||||||
rails-settings-cached (~> 2.8.3)
|
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
|
||||||
|
solargraph
|
||||||
sprockets-rails
|
sprockets-rails
|
||||||
sqlite3 (~> 1.4)
|
sqlite3 (~> 1.4)
|
||||||
stimulus-rails
|
stimulus-rails
|
||||||
|
14
README.md
14
README.md
@ -14,7 +14,7 @@ so:
|
|||||||
|
|
||||||
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||||
Docker Desktop)
|
Docker Desktop)
|
||||||
2. Uncomment the `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"`
|
||||||
@ -23,9 +23,8 @@ so:
|
|||||||
|
|
||||||
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).
|
||||||
|
|
||||||
Log in with username "admin" and password "admin is admin". All users listed on
|
Log in with username "admin" and password "admin is admin". All users listed on
|
||||||
[http://localhost:3000/admin/ldap_users](http://localhost:3000/admin/ldap_users)
|
[http://localhost:3000/admin/users](http://localhost:3000/admin/users)
|
||||||
have the password "user is user".
|
have the password "user is user".
|
||||||
|
|
||||||
### Rails app
|
### Rails app
|
||||||
@ -79,6 +78,15 @@ The setup task will first delete any existing entries in the directory tree
|
|||||||
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
|
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
|
||||||
with a fresh installation, delete both that directory as well as the container.
|
with a fresh installation, delete both that directory as well as the container.
|
||||||
|
|
||||||
|
### Solargraph
|
||||||
|
|
||||||
|
[Solargraph](https://solargraph.org/) is a Ruby language server, which you may
|
||||||
|
use with your editor to add features like auto-completion and syntax
|
||||||
|
validation. You can add inline documentation for bundled gems with this
|
||||||
|
command:
|
||||||
|
|
||||||
|
bundle exec yard gems
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### Rails
|
### Rails
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
|
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
|
||||||
|
<% if @positioning == :vertical %>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
|
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
|
||||||
<%= @title %>
|
<%= @title %>
|
||||||
@ -10,4 +11,19 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
</label>
|
</label>
|
||||||
|
<% elsif @positioning == :horizontal %>
|
||||||
|
<label class="block flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="font-bold mb-1"><%= @title %></label>
|
||||||
|
<% if @descripton.present? %>
|
||||||
|
<p class="text-gray-500"><%= @descripton %></p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||||
|
<%= content %>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<% else %>
|
||||||
|
<p>Invalid <code>positioning<code> argument for <code>FieldsetComponent</code>.</p>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
module FormElements
|
module FormElements
|
||||||
class FieldsetComponent < ViewComponent::Base
|
class FieldsetComponent < ViewComponent::Base
|
||||||
def initialize(tag: "li", title:, description: nil)
|
def initialize(tag: "li", positioning: :vertical, title:, description: nil)
|
||||||
@tag = tag
|
@tag = tag
|
||||||
@title = title
|
@positioning = positioning
|
||||||
@descripton = description
|
@title = title
|
||||||
|
@descripton = description
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
|
<%= tag.public_send @tag, class: "flex items-center justify-between mb-6 last:mb-0",
|
||||||
data: @form.present? ? {
|
data: @form_enabled ? {
|
||||||
controller: "settings--toggle",
|
controller: "settings--toggle",
|
||||||
:'settings--toggle-switch-enabled-value' => @enabled.to_s
|
:'settings--toggle-switch-enabled-value' => @enabled.to_s
|
||||||
} : nil do %>
|
} : nil do %>
|
||||||
@ -11,16 +11,23 @@
|
|||||||
<%= render FormElements::ToggleComponent.new(
|
<%= render FormElements::ToggleComponent.new(
|
||||||
enabled: @enabled,
|
enabled: @enabled,
|
||||||
input_enabled: @input_enabled,
|
input_enabled: @input_enabled,
|
||||||
class_names: @form.present? ? "hidden" : nil,
|
class_names: @form_enabled ? "hidden" : nil,
|
||||||
data: {
|
data: {
|
||||||
:'settings--toggle-target' => "button",
|
:'settings--toggle-target' => "button",
|
||||||
action: "settings--toggle#toggleSwitch"
|
action: "settings--toggle#toggleSwitch"
|
||||||
}) %>
|
}) %>
|
||||||
<% if @form.present? %>
|
<% if @form_enabled %>
|
||||||
<%= @form.check_box @attribute, {
|
<% if @attribute.present? %>
|
||||||
checked: @enabled,
|
<%= @form.check_box @attribute, {
|
||||||
data: { :'settings--toggle-target' => "checkbox" }
|
checked: @enabled,
|
||||||
}, "true", "false" %>
|
data: { :'settings--toggle-target' => "checkbox" }
|
||||||
|
}, "true", "false" %>
|
||||||
|
<% else %>
|
||||||
|
<input name="<%= @field_name %>" type="hidden" value="false" autocomplete="off">
|
||||||
|
<%= check_box_tag @field_name, "true", @enabled, {
|
||||||
|
data: { :'settings--toggle-target' => "checkbox" }
|
||||||
|
} %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
module FormElements
|
module FormElements
|
||||||
class FieldsetToggleComponent < ViewComponent::Base
|
class FieldsetToggleComponent < ViewComponent::Base
|
||||||
def initialize(form: nil, attribute: nil, tag: "li", enabled: false,
|
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
|
||||||
input_enabled: true, title:, description:)
|
enabled: false, input_enabled: true, title:, description:)
|
||||||
|
@tag = tag
|
||||||
@form = form
|
@form = form
|
||||||
@attribute = attribute
|
@attribute = attribute
|
||||||
@tag = tag
|
@field_name = field_name
|
||||||
|
@form_enabled = @form.present? || @field_name.present?
|
||||||
@enabled = enabled
|
@enabled = enabled
|
||||||
@input_enabled = input_enabled
|
@input_enabled = input_enabled
|
||||||
@title = title
|
@title = title
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<%= button_tag type: "button", name: "toggle", data: @data,
|
<%= button_tag type: "button", name: "toggle", data: @data,
|
||||||
role: "switch", aria: { checked: @enabled.to_s },
|
role: "switch", aria: { checked: @enabled.to_s },
|
||||||
disabled: !@input_enabled,
|
tabindex: @tabindex, disabled: !@input_enabled,
|
||||||
class: "#{ @enabled ? 'bg-blue-600' : 'bg-gray-200' }
|
class: "#{ @enabled ? 'bg-blue-600' : 'bg-gray-200' }
|
||||||
#{ @class_names.present? ? @class_names : '' }
|
#{ @class_names.present? ? @class_names : '' }
|
||||||
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer
|
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
module FormElements
|
module FormElements
|
||||||
class ToggleComponent < ViewComponent::Base
|
class ToggleComponent < ViewComponent::Base
|
||||||
def initialize(enabled:, input_enabled: true, data: nil, class_names: nil)
|
def initialize(enabled:, input_enabled: true, data: nil, class_names: nil, tabindex: nil)
|
||||||
@enabled = !!enabled
|
@enabled = !!enabled
|
||||||
@input_enabled = input_enabled
|
@input_enabled = input_enabled
|
||||||
@data = data
|
@data = data
|
||||||
@class_names = class_names
|
@class_names = class_names
|
||||||
|
@tabindex = tabindex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
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
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
class Settings::AccountController < SettingsController
|
|
||||||
|
|
||||||
def index
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_password
|
|
||||||
current_user.send_reset_password_instructions
|
|
||||||
sign_out current_user
|
|
||||||
msg = "We have sent you an email with a link to reset your password."
|
|
||||||
redirect_to check_your_email_path, notice: msg
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -1,11 +0,0 @@
|
|||||||
class Settings::ProfileController < SettingsController
|
|
||||||
|
|
||||||
def index
|
|
||||||
@user = current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
@ -1,13 +1,52 @@
|
|||||||
class SettingsController < ApplicationController
|
class SettingsController < ApplicationController
|
||||||
before_action :require_user_signed_in
|
before_action :authenticate_user!
|
||||||
before_action :set_current_section
|
before_action :set_main_nav_section
|
||||||
|
before_action :set_settings_section, only: ['show', 'update']
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
redirect_to setting_path(:profile)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@user = current_user
|
||||||
|
@user.preferences.merge! user_params[:preferences]
|
||||||
|
@user.save!
|
||||||
|
|
||||||
|
redirect_to setting_path(@settings_section), flash: {
|
||||||
|
success: 'Settings saved.'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_password
|
||||||
|
current_user.send_reset_password_instructions
|
||||||
|
sign_out current_user
|
||||||
|
msg = "We have sent you an email with a link to reset your password."
|
||||||
|
redirect_to check_your_email_path, notice: msg
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_current_section
|
def set_main_nav_section
|
||||||
@current_section = :settings
|
@current_section = :settings
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_settings_section
|
||||||
|
@settings_section = params[:section]
|
||||||
|
allowed_sections = [:profile, :account, :lightning, :xmpp]
|
||||||
|
|
||||||
|
unless allowed_sections.include?(@settings_section.to_sym)
|
||||||
|
redirect_to setting_path(:profile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_params
|
||||||
|
params.require(:user).permit(preferences: [
|
||||||
|
:lightning_notify_sats_received,
|
||||||
|
:xmpp_exchange_contacts_with_invitees
|
||||||
|
])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
57
app/controllers/webfinger_controller.rb
Normal file
57
app/controllers/webfinger_controller.rb
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
class WebfingerController < ApplicationController
|
||||||
|
before_action :allow_cross_origin_requests, only: [:show]
|
||||||
|
|
||||||
|
layout false
|
||||||
|
|
||||||
|
def show
|
||||||
|
resource = params[:resource]
|
||||||
|
|
||||||
|
if resource && resource.match(/acct:\w+/)
|
||||||
|
useraddress = resource.split(":").last
|
||||||
|
username, org = useraddress.split("@")
|
||||||
|
username.downcase!
|
||||||
|
unless User.where(cn: username, ou: org).any?
|
||||||
|
head 404 and return
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: webfinger(useraddress).to_json,
|
||||||
|
content_type: "application/jrd+json"
|
||||||
|
else
|
||||||
|
head 422 and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def webfinger(useraddress)
|
||||||
|
links = [];
|
||||||
|
|
||||||
|
links << remotestorage_link(useraddress) if Setting.remotestorage_enabled
|
||||||
|
|
||||||
|
{ "links" => links }
|
||||||
|
end
|
||||||
|
|
||||||
|
def remotestorage_link(useraddress)
|
||||||
|
# TODO use when OAuth routes are available
|
||||||
|
# auth_url = new_rs_oauth_url(useraddress)
|
||||||
|
auth_url = "https://example.com/rs/oauth"
|
||||||
|
storage_url = "#{Setting.rs_storage_url}/#{useraddress}"
|
||||||
|
|
||||||
|
{
|
||||||
|
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
|
||||||
|
"href" => storage_url,
|
||||||
|
"properties" => {
|
||||||
|
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
|
||||||
|
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
|
||||||
|
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
|
||||||
|
"http://tools.ietf.org/html/rfc7233": "GET", # content range requests
|
||||||
|
"http://remotestorage.io/spec/web-authoring": nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_cross_origin_requests
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
|
||||||
|
end
|
||||||
|
end
|
@ -12,22 +12,28 @@ class WebhooksController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
user = User.find_by!(ln_account: payload[:user_login])
|
user = User.find_by!(ln_account: payload[:user_login])
|
||||||
|
notify = user.preferences[:lightning_notify_sats_received]
|
||||||
# TODO make configurable
|
case notify
|
||||||
notify_xmpp(user.address, payload[:amount], payload[:memo])
|
when "xmpp"
|
||||||
|
notify_xmpp(user.address, payload[:amount], payload[:memo])
|
||||||
|
when "email"
|
||||||
|
NotificationMailer.with(user: user, amount_sats: payload[:amount])
|
||||||
|
.lightning_sats_received.deliver_later
|
||||||
|
end
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# TODO refactor into mailer-like generic class/service
|
||||||
def notify_xmpp(address, amt_sats, memo)
|
def notify_xmpp(address, amt_sats, memo)
|
||||||
payload = {
|
payload = {
|
||||||
type: "normal",
|
type: "normal",
|
||||||
from: "kosmos.org", # TODO domain config
|
from: "kosmos.org", # TODO domain config
|
||||||
to: address,
|
to: address,
|
||||||
subject: "Sats received!",
|
subject: "Sats received!",
|
||||||
body: "#{amt_sats} sats received in your Lightning wallet:\n> #{memo}"
|
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
|
||||||
}
|
}
|
||||||
XmppSendMessageJob.perform_later(payload)
|
XmppSendMessageJob.perform_later(payload)
|
||||||
end
|
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(() => {
|
||||||
|
8
app/mailers/notification_mailer.rb
Normal file
8
app/mailers/notification_mailer.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class NotificationMailer < ApplicationMailer
|
||||||
|
def lightning_sats_received
|
||||||
|
@user = params[:user]
|
||||||
|
@amount_sats = params[:amount_sats]
|
||||||
|
@subject = "Sats received"
|
||||||
|
mail to: @user.email, subject: @subject
|
||||||
|
end
|
||||||
|
end
|
@ -2,6 +2,13 @@
|
|||||||
class Setting < RailsSettings::Base
|
class Setting < RailsSettings::Base
|
||||||
cache_prefix { "v1" }
|
cache_prefix { "v1" }
|
||||||
|
|
||||||
|
#
|
||||||
|
# Internal services
|
||||||
|
#
|
||||||
|
|
||||||
|
field :redis_url, type: :string, readonly: true,
|
||||||
|
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Registrations
|
# Registrations
|
||||||
#
|
#
|
||||||
@ -10,6 +17,13 @@ class Setting < RailsSettings::Base
|
|||||||
account accounts donations mail webmaster support
|
account accounts donations mail webmaster support
|
||||||
]
|
]
|
||||||
|
|
||||||
|
#
|
||||||
|
# Sentry
|
||||||
|
#
|
||||||
|
|
||||||
|
field :sentry_enabled, type: :boolean, readonly: true,
|
||||||
|
default: (ENV["SENTRY_DSN"].present?.to_s || false)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Discourse
|
# Discourse
|
||||||
#
|
#
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
include EmailValidatable
|
include EmailValidatable
|
||||||
|
|
||||||
|
serialize :preferences, UserPreferences
|
||||||
|
|
||||||
# Relations
|
# Relations
|
||||||
has_many :invitations, dependent: :destroy
|
has_many :invitations, dependent: :destroy
|
||||||
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
|
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
|
||||||
@ -24,6 +26,7 @@ class User < ApplicationRecord
|
|||||||
validates_format_of :cn, without: /\A-/,
|
validates_format_of :cn, without: /\A-/,
|
||||||
if: Proc.new{ |u| u.cn.present? },
|
if: Proc.new{ |u| u.cn.present? },
|
||||||
message: "is invalid. Usernames need to start with a letter."
|
message: "is invalid. Usernames need to start with a letter."
|
||||||
|
# FIXME This needs a server restart to apply values
|
||||||
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
|
validates_format_of :cn, without: /\A(#{Setting.reserved_usernames.join('|')})\z/i,
|
||||||
message: "has already been taken"
|
message: "has already been taken"
|
||||||
|
|
||||||
@ -40,7 +43,9 @@ class User < ApplicationRecord
|
|||||||
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
|
||||||
@ -55,16 +60,23 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def devise_after_confirmation
|
def devise_after_confirmation
|
||||||
enable_service %w[ discourse ejabberd gitea mediawiki ]
|
enable_service %w[ discourse gitea mediawiki xmpp ]
|
||||||
|
|
||||||
#TODO enable in development when we have easy setup of ejabberd etc.
|
#TODO enable in development when we have easy setup of ejabberd etc.
|
||||||
return if Rails.env.development?
|
return if Rails.env.development?
|
||||||
|
|
||||||
if inviter.present?
|
if inviter.present?
|
||||||
exchange_xmpp_contact_with_inviter if Setting.ejabberd_enabled?
|
if Setting.ejabberd_enabled? &&
|
||||||
|
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
|
||||||
|
exchange_xmpp_contact_with_inviter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_devise_notification(notification, *args)
|
||||||
|
devise_mailer.send(notification, self, *args).deliver_later
|
||||||
|
end
|
||||||
|
|
||||||
def reset_password(new_password, new_password_confirmation)
|
def reset_password(new_password, new_password_confirmation)
|
||||||
self.password = new_password
|
self.password = new_password
|
||||||
self.password_confirmation = new_password_confirmation
|
self.password_confirmation = new_password_confirmation
|
||||||
@ -130,8 +142,8 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def exchange_xmpp_contact_with_inviter
|
def exchange_xmpp_contact_with_inviter
|
||||||
return unless inviter.services_enabled.include?("ejabberd") &&
|
return unless inviter.services_enabled.include?("xmpp") &&
|
||||||
services_enabled.include?("ejabberd")
|
services_enabled.include?("xmpp")
|
||||||
XmppExchangeContactsJob.perform_later(inviter, self.cn, self.ou)
|
XmppExchangeContactsJob.perform_later(inviter, self.cn, self.ou)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
29
app/models/user_preferences.rb
Normal file
29
app/models/user_preferences.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
DEFAULT_PREFS = YAML.load_file("#{Rails.root}/config/default_preferences.yml")
|
||||||
|
|
||||||
|
class UserPreferences
|
||||||
|
def self.dump(value)
|
||||||
|
process(value).to_yaml
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load(string)
|
||||||
|
stored_prefs = YAML.load(string || "{}")
|
||||||
|
DEFAULT_PREFS.merge(stored_prefs).with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.is_integer?(value)
|
||||||
|
value.to_i.to_s == value
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.process(hash)
|
||||||
|
hash.each do |key, value|
|
||||||
|
if value == "true"
|
||||||
|
hash[key] = true
|
||||||
|
elsif value == "false"
|
||||||
|
hash[key] = false
|
||||||
|
elsif value.is_a?(String) && is_integer?(value)
|
||||||
|
hash[key] = value.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
hash.stringify_keys!.to_h
|
||||||
|
end
|
||||||
|
end
|
@ -1,6 +1,6 @@
|
|||||||
class EjabberdApiClient
|
class EjabberdApiClient
|
||||||
def initialize
|
def initialize
|
||||||
@base_url = ENV["EJABBERD_API_URL"]
|
@base_url = Setting.ejabberd_api_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def post(endpoint, payload)
|
def post(endpoint, payload)
|
||||||
|
@ -135,7 +135,7 @@
|
|||||||
<td>XMPP (ejabberd)</td>
|
<td>XMPP (ejabberd)</td>
|
||||||
<td>
|
<td>
|
||||||
<%= render FormElements::ToggleComponent.new(
|
<%= render FormElements::ToggleComponent.new(
|
||||||
enabled: @services_enabled.include?("ejabberd"),
|
enabled: @services_enabled.include?("xmpp"),
|
||||||
input_enabled: false
|
input_enabled: false
|
||||||
) %>
|
) %>
|
||||||
</td>
|
</td>
|
||||||
|
@ -7,19 +7,43 @@
|
|||||||
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
||||||
<p class="flex gap-2 items-center">
|
<p class="flex gap-2 items-center">
|
||||||
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
<%= f.text_field :cn, autofocus: true, autocomplete: "username",
|
||||||
required: true, class: "relative grow"%>
|
required: true, class: "relative grow", tabindex: "1" %>
|
||||||
<span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
|
<span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p class="mb-8">
|
||||||
<%= f.label :password, class: 'block mb-2 font-bold' %>
|
<%= f.label :password, class: 'block mb-2 font-bold' %>
|
||||||
<%= f.password_field :password, autocomplete: "current-password",
|
<%= f.password_field :password, autocomplete: "current-password",
|
||||||
required: true, class: "w-full"%>
|
required: true, class: "w-full", tabindex: "2" %>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-8">
|
|
||||||
<%= f.submit "Log in", class: 'btn-md btn-blue w-full' %>
|
<%= 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-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell <%= custom_class %>"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 342 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-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle <%= custom_class %>"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 449 B |
@ -0,0 +1,3 @@
|
|||||||
|
You just received <%= number_with_delimiter @amount_sats %> sats in your Lightning account (<%= @user.address %>). Check your wallet app, or open the account page for details:
|
||||||
|
|
||||||
|
<%= wallet_transactions_url %>
|
19
app/views/settings/_account.html.erb
Normal file
19
app/views/settings/_account.html.erb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<section>
|
||||||
|
<h3>E-Mail</h3>
|
||||||
|
<p class="mb-2">
|
||||||
|
<%= label :email, 'Address', class: 'font-bold' %>
|
||||||
|
</p>
|
||||||
|
<p class="flex gap-1 mb-2 sm:w-3/5">
|
||||||
|
<input type="text" id="email" class="grow"
|
||||||
|
value=<%= current_user.email %> disabled="disabled" />
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Password</h3>
|
||||||
|
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
|
||||||
|
<%= form_with(url: reset_password_settings_path, method: :post) do %>
|
||||||
|
<p>
|
||||||
|
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
25
app/views/settings/_lightning.html.erb
Normal file
25
app/views/settings/_lightning.html.erb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<%= form_for @user, url: setting_path(:lightning), html: { :method => :put } do |f| %>
|
||||||
|
<section>
|
||||||
|
<h3>Notifications</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
positioning: :horizontal,
|
||||||
|
title: "Sats received",
|
||||||
|
description: "Notify me when sats are sent to my Lightning Address"
|
||||||
|
) do %>
|
||||||
|
<% f.fields_for :preferences do |p| %>
|
||||||
|
<%= p.select :lightning_notify_sats_received, options_for_select([
|
||||||
|
["off", "disabled"],
|
||||||
|
["Chat (Jabber)", "xmpp"],
|
||||||
|
["E-Mail", "email"]
|
||||||
|
], selected: @user.preferences[:lightning_notify_sats_received]) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<p class="pt-6 border-t border-gray-200 text-right">
|
||||||
|
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
16
app/views/settings/_notifications.html.erb
Normal file
16
app/views/settings/_notifications.html.erb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<section>
|
||||||
|
<h3>Lightning Wallet</h3>
|
||||||
|
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
positioning: :horizontal,
|
||||||
|
title: "Sats received",
|
||||||
|
description: "Notify when sats are sent to my Lightning Address"
|
||||||
|
) do %>
|
||||||
|
<%= select_tag :sats_received, options_for_select([
|
||||||
|
["off", "off"],
|
||||||
|
["Chat (Jabber)", "xmpp"]
|
||||||
|
]) %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</section>
|
30
app/views/settings/_profile.html.erb
Normal file
30
app/views/settings/_profile.html.erb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<section>
|
||||||
|
<h3>Profile</h3>
|
||||||
|
<p class="mb-2">
|
||||||
|
<%= label :user_address, 'User address', class: 'font-bold' %>
|
||||||
|
</p>
|
||||||
|
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
|
||||||
|
<input type="text" id="user_address" class="grow"
|
||||||
|
value=<%= @user.address %> disabled="disabled"
|
||||||
|
data-clipboard-target="source" />
|
||||||
|
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
|
||||||
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<span class="content-initial">
|
||||||
|
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
<span class="content-active hidden">
|
||||||
|
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Your user address for Chat and Lightning Network.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
|
||||||
|
<%# <p class="mt-8">
|
||||||
|
<%# <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
|
||||||
|
<%# </p>
|
||||||
|
<%# <% end %>
|
||||||
|
</section>
|
18
app/views/settings/_xmpp.html.erb
Normal file
18
app/views/settings/_xmpp.html.erb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<%= form_for @user, url: setting_path(:xmpp), html: { :method => :put } do |f| %>
|
||||||
|
<section>
|
||||||
|
<h3>Contacts</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
field_name: "user[preferences][xmpp_exchange_contacts_with_invitees]",
|
||||||
|
enabled: @user.preferences[:xmpp_exchange_contacts_with_invitees],
|
||||||
|
title: "Exchange contacts when invited user signs up",
|
||||||
|
description: "Add each others contacts, so you can chat with them immediately"
|
||||||
|
) %>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<p class="pt-6 border-t border-gray-200 text-right">
|
||||||
|
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
@ -1,23 +0,0 @@
|
|||||||
<%= render HeaderComponent.new(title: "Settings") %>
|
|
||||||
|
|
||||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
|
|
||||||
<section>
|
|
||||||
<h3>E-Mail</h3>
|
|
||||||
<p class="mb-2">
|
|
||||||
<%= label :email, 'Address', class: 'font-bold' %>
|
|
||||||
</p>
|
|
||||||
<p class="flex gap-1 mb-2 sm:w-3/5">
|
|
||||||
<input type="text" id="email" class="grow"
|
|
||||||
value=<%= current_user.email %> disabled="disabled" />
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h3>Password</h3>
|
|
||||||
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
|
|
||||||
<%= form_with(url: settings_reset_password_path, method: :post) do %>
|
|
||||||
<p>
|
|
||||||
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
@ -1,34 +0,0 @@
|
|||||||
<%= render HeaderComponent.new(title: "Settings") %>
|
|
||||||
|
|
||||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
|
|
||||||
<section>
|
|
||||||
<h3>Profile</h3>
|
|
||||||
<p class="mb-2">
|
|
||||||
<%= label :user_address, 'User address', class: 'font-bold' %>
|
|
||||||
</p>
|
|
||||||
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
|
|
||||||
<input type="text" id="user_address" class="grow"
|
|
||||||
value=<%= @user.address %> disabled="disabled"
|
|
||||||
data-clipboard-target="source" />
|
|
||||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
|
|
||||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
|
||||||
title="Copy to clipboard">
|
|
||||||
<span class="content-initial">
|
|
||||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
|
||||||
</span>
|
|
||||||
<span class="content-active hidden">
|
|
||||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-gray-500">
|
|
||||||
Your user address for Chat and Lightning Network.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
|
|
||||||
<%# <p class="mt-8">
|
|
||||||
<%# <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
|
|
||||||
<%# </p>
|
|
||||||
<%# <% end %>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
5
app/views/settings/show.html.erb
Normal file
5
app/views/settings/show.html.erb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Settings") %>
|
||||||
|
|
||||||
|
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/sidenav_settings') do %>
|
||||||
|
<%= render partial: @settings_section %>
|
||||||
|
<% end %>
|
@ -6,5 +6,5 @@
|
|||||||
class: main_nav_class(@current_section, :invitations) %>
|
class: main_nav_class(@current_section, :invitations) %>
|
||||||
<%= link_to "Wallet", wallet_path,
|
<%= link_to "Wallet", wallet_path,
|
||||||
class: main_nav_class(@current_section, :wallet) %>
|
class: main_nav_class(@current_section, :wallet) %>
|
||||||
<%= link_to "Settings", settings_profile_path,
|
<%= link_to "Settings", settings_path,
|
||||||
class: main_nav_class(@current_section, :settings) %>
|
class: main_nav_class(@current_section, :settings) %>
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Profile", path: settings_profile_path, icon: "user",
|
name: "Profile", path: setting_path(:profile), icon: "user",
|
||||||
active: current_page?(settings_profile_path)
|
active: current_page?(setting_path(:profile))
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Account", path: settings_account_path, icon: "key",
|
name: "Account", path: setting_path(:account), icon: "key",
|
||||||
active: current_page?(settings_account_path)
|
active: current_page?(setting_path(:account))
|
||||||
) %>
|
) %>
|
||||||
|
<% if Setting.ejabberd_enabled %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Security", path: "#", icon: "shield", disabled: true
|
name: "Chat", path: setting_path(:xmpp), icon: "message-circle",
|
||||||
|
active: current_page?(setting_path(:xmpp))
|
||||||
) %>
|
) %>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.lndhub_enabled %>
|
||||||
|
<%= render SidenavLinkComponent.new(
|
||||||
|
name: "Wallet", path: setting_path(:lightning), icon: "zap",
|
||||||
|
active: current_page?(setting_path(:lightning))
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
2
config/default_preferences.yml
Normal file
2
config/default_preferences.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
lightning_notify_sats_received: disabled # or xmpp, email
|
||||||
|
xmpp_exchange_contacts_with_invitees: true
|
@ -62,6 +62,11 @@ Rails.application.configure do
|
|||||||
outgoing_email_address = ENV.fetch('SMTP_FROM_ADDRESS', 'accounts@localhost')
|
outgoing_email_address = ENV.fetch('SMTP_FROM_ADDRESS', 'accounts@localhost')
|
||||||
outgoing_email_domain = Mail::Address.new(outgoing_email_address).domain
|
outgoing_email_domain = Mail::Address.new(outgoing_email_address).domain
|
||||||
|
|
||||||
|
config.action_mailer.default_url_options = {
|
||||||
|
host: ENV['AKKOUNTS_DOMAIN'],
|
||||||
|
protocol: "https",
|
||||||
|
}
|
||||||
|
|
||||||
config.action_mailer.default_options = {
|
config.action_mailer.default_options = {
|
||||||
from: outgoing_email_address,
|
from: outgoing_email_address,
|
||||||
message_id: -> { "<#{Mail.random_tag}@#{outgoing_email_domain}>" },
|
message_id: -> { "<#{Mail.random_tag}@#{outgoing_email_domain}>" },
|
||||||
|
@ -186,13 +186,13 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# ==> Configuration for :rememberable
|
# ==> Configuration for :rememberable
|
||||||
# The time the user will be remembered without asking for credentials again.
|
# The time the user will be remembered without asking for credentials again.
|
||||||
# config.remember_for = 2.weeks
|
config.remember_for = 2.weeks
|
||||||
|
|
||||||
# Invalidates all the remember me tokens when the user signs out.
|
# Invalidates all the remember me tokens when the user signs out.
|
||||||
config.expire_all_remember_me_on_sign_out = true
|
config.expire_all_remember_me_on_sign_out = true
|
||||||
|
|
||||||
# If true, extends the user's remember period when remembered via cookie.
|
# If true, extends the user's remember period when remembered via cookie.
|
||||||
# config.extend_remember_period = false
|
config.extend_remember_period = true
|
||||||
|
|
||||||
# Options to be passed to the created cookie. For instance, you can set
|
# Options to be passed to the created cookie. For instance, you can set
|
||||||
# secure: true in order to force SSL only cookies.
|
# secure: true in order to force SSL only cookies.
|
||||||
@ -210,7 +210,7 @@ Devise.setup do |config|
|
|||||||
# ==> Configuration for :timeoutable
|
# ==> Configuration for :timeoutable
|
||||||
# The time you want to timeout the user session without activity. After this
|
# The time you want to timeout the user session without activity. After this
|
||||||
# time the user will be asked for credentials again. Default is 30 minutes.
|
# time the user will be asked for credentials again. Default is 30 minutes.
|
||||||
# config.timeout_in = 30.minutes
|
config.timeout_in = 30.minutes
|
||||||
|
|
||||||
# ==> Configuration for :lockable
|
# ==> Configuration for :lockable
|
||||||
# Defines which strategy will be used to lock an account.
|
# Defines which strategy will be used to lock an account.
|
||||||
|
9
config/initializers/sentry.rb
Normal file
9
config/initializers/sentry.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
if ENV["SENTRY_DSN"].present?
|
||||||
|
Sentry.init do |config|
|
||||||
|
config.dsn = ENV["SENTRY_DSN"]
|
||||||
|
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
|
||||||
|
config.traces_sampler = lambda do |context|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
config/initializers/sidekiq.rb
Normal file
5
config/initializers/sidekiq.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
require_relative "../../app/models/setting"
|
||||||
|
|
||||||
|
Sidekiq.configure_server do |config|
|
||||||
|
config.redis = { url: Setting.redis_url }
|
||||||
|
end
|
@ -1,7 +1,7 @@
|
|||||||
require 'sidekiq/web'
|
require 'sidekiq/web'
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
devise_for :users, :controllers => { :confirmations => "users/confirmations" }
|
devise_for :users, controllers: { confirmations: "users/confirmations" }
|
||||||
|
|
||||||
get 'welcome', to: 'welcome#index'
|
get 'welcome', to: 'welcome#index'
|
||||||
get 'check_your_email', to: 'welcome#check_your_email'
|
get 'check_your_email', to: 'welcome#check_your_email'
|
||||||
@ -10,13 +10,6 @@ Rails.application.routes.draw do
|
|||||||
match 'signup/:step', to: 'signup#steps', as: :signup_steps, via: [:get, :post]
|
match 'signup/:step', to: 'signup#steps', as: :signup_steps, via: [:get, :post]
|
||||||
post 'signup_validate', to: 'signup#validate'
|
post 'signup_validate', to: 'signup#validate'
|
||||||
|
|
||||||
namespace :settings do
|
|
||||||
get 'profile', to: 'profile#index'
|
|
||||||
post 'profile', to: 'profile#update'
|
|
||||||
get 'account', to: 'account#index'
|
|
||||||
post 'reset_password', to: 'account#reset_password'
|
|
||||||
end
|
|
||||||
|
|
||||||
namespace :contributions do
|
namespace :contributions do
|
||||||
root to: 'donations#index'
|
root to: 'donations#index'
|
||||||
get 'projects', to: 'projects#index'
|
get 'projects', to: 'projects#index'
|
||||||
@ -28,6 +21,12 @@ Rails.application.routes.draw do
|
|||||||
get 'wallet', to: 'wallet#index'
|
get 'wallet', to: 'wallet#index'
|
||||||
get 'wallet/transactions', to: 'wallet#transactions'
|
get 'wallet/transactions', to: 'wallet#transactions'
|
||||||
|
|
||||||
|
resources :settings, param: 'section', only: ['index', 'show', 'update'] do
|
||||||
|
collection do
|
||||||
|
post 'reset_password'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get 'lnurlpay/:address', to: 'lnurlpay#index',
|
get 'lnurlpay/:address', to: 'lnurlpay#index',
|
||||||
as: 'lightning_address', constraints: { address: /[^\/]+/}
|
as: 'lightning_address', constraints: { address: /[^\/]+/}
|
||||||
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice',
|
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice',
|
||||||
@ -60,6 +59,9 @@ Rails.application.routes.draw do
|
|||||||
get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app
|
get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get ".well-known/webfinger" => "webfinger#show"
|
||||||
|
|
||||||
|
|
||||||
authenticate :user, ->(user) { user.is_admin? } do
|
authenticate :user, ->(user) { user.is_admin? } do
|
||||||
mount Sidekiq::Web => '/sidekiq'
|
mount Sidekiq::Web => '/sidekiq'
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
:concurrency: 2
|
:concurrency: 2
|
||||||
:queues:
|
:queues:
|
||||||
- default
|
- default
|
||||||
|
- mailers
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
class AddRememberCreatedAtToUsers < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :remember_created_at, :datetime
|
||||||
|
add_column :users, :remember_token, :string
|
||||||
|
end
|
||||||
|
end
|
5
db/migrate/20230403135149_add_preferences_to_users.rb
Normal file
5
db/migrate/20230403135149_add_preferences_to_users.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AddPreferencesToUsers < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :preferences, :text
|
||||||
|
end
|
||||||
|
end
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_03_12_212030) do
|
ActiveRecord::Schema[7.0].define(version: 2023_04_03_150200) do
|
||||||
create_table "donations", force: :cascade do |t|
|
create_table "donations", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.integer "amount_sats"
|
t.integer "amount_sats"
|
||||||
@ -71,6 +71,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_12_212030) do
|
|||||||
t.text "ln_login_ciphertext"
|
t.text "ln_login_ciphertext"
|
||||||
t.text "ln_password_ciphertext"
|
t.text "ln_password_ciphertext"
|
||||||
t.string "ln_account"
|
t.string "ln_account"
|
||||||
|
t.string "nostr_pubkey"
|
||||||
|
t.datetime "remember_created_at"
|
||||||
|
t.string "remember_token"
|
||||||
|
t.text "preferences"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
@ -3,11 +3,67 @@ services:
|
|||||||
image: 4teamwork/389ds:latest
|
image: 4teamwork/389ds:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./tmp/389ds:/data
|
- ./tmp/389ds:/data
|
||||||
|
networks:
|
||||||
|
- external_network
|
||||||
|
- internal_network
|
||||||
ports:
|
ports:
|
||||||
- "389:3389"
|
- "389:3389"
|
||||||
environment:
|
environment:
|
||||||
DS_DM_PASSWORD: passthebutter
|
DS_DM_PASSWORD: passthebutter
|
||||||
SUFFIX_NAME: "dc=kosmos,dc=org"
|
SUFFIX_NAME: "dc=kosmos,dc=org"
|
||||||
|
|
||||||
|
# redis:
|
||||||
|
# restart: always
|
||||||
|
# image: redis:7-alpine
|
||||||
|
# networks:
|
||||||
|
# - internal_network
|
||||||
|
# healthcheck:
|
||||||
|
# test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
# volumes:
|
||||||
|
# - ./tmp/redis:/data
|
||||||
|
|
||||||
|
# web:
|
||||||
|
# build: .
|
||||||
|
# tty: true
|
||||||
|
# command: bash -c "rm -f /akkounts/tmp/pids/server.pid; bin/dev"
|
||||||
|
# volumes:
|
||||||
|
# - .:/akkounts
|
||||||
|
# networks:
|
||||||
|
# - external_network
|
||||||
|
# - internal_network
|
||||||
|
# ports:
|
||||||
|
# - "3000:3000"
|
||||||
|
# environment:
|
||||||
|
# RAILS_ENV: development
|
||||||
|
# REDIS_URL: redis://redis:6379/0
|
||||||
|
# LDAP_HOST: ldap
|
||||||
|
# LDAP_PORT: 3389
|
||||||
|
# LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
|
# LDAP_USE_TLS: "false"
|
||||||
|
# depends_on:
|
||||||
|
# - ldap
|
||||||
|
# - redis
|
||||||
|
|
||||||
|
# sidekiq:
|
||||||
|
# build: .
|
||||||
|
# command: bash -c "bundle exec sidekiq -C config/sidekiq.yml"
|
||||||
|
# volumes:
|
||||||
|
# - .:/akkounts
|
||||||
|
# networks:
|
||||||
|
# - internal_network
|
||||||
|
# environment:
|
||||||
|
# RAILS_ENV: development
|
||||||
|
# REDIS_URL: redis://redis:6379/0
|
||||||
|
# LDAP_HOST: ldap
|
||||||
|
# LDAP_PORT: 3389
|
||||||
|
# LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
|
# LDAP_USE_TLS: "false"
|
||||||
|
# LAUNCHY_DRY_RUN: true
|
||||||
|
# BROWSER: /dev/null
|
||||||
|
# depends_on:
|
||||||
|
# - ldap
|
||||||
|
# - redis
|
||||||
|
|
||||||
# phpldapadmin:
|
# phpldapadmin:
|
||||||
# image: osixia/phpldapadmin:0.9.0
|
# image: osixia/phpldapadmin:0.9.0
|
||||||
# ports:
|
# ports:
|
||||||
@ -16,19 +72,8 @@ services:
|
|||||||
# PHPLDAPADMIN_HTTPS: false
|
# PHPLDAPADMIN_HTTPS: false
|
||||||
# PHPLDAPADMIN_LDAP_HOSTS: "#PYTHON2BASH:[{'ldap': [{'server': [{'tls': False}, {'port': 3389}]}, {'login': [{'bind_id': 'cn=Directory Manager'}, {'bind_pass': 'passthebutter'}]}]}]"
|
# PHPLDAPADMIN_LDAP_HOSTS: "#PYTHON2BASH:[{'ldap': [{'server': [{'tls': False}, {'port': 3389}]}, {'login': [{'bind_id': 'cn=Directory Manager'}, {'bind_pass': 'passthebutter'}]}]}]"
|
||||||
# PHPLDAPADMIN_LDAP_CLIENT_TLS: false
|
# PHPLDAPADMIN_LDAP_CLIENT_TLS: false
|
||||||
# web:
|
|
||||||
# build: .
|
networks:
|
||||||
# tty: true
|
external_network:
|
||||||
# command: bash -c "sleep 5 && rm -f tmp/pids/server.pid && bin/dev"
|
internal_network:
|
||||||
# volumes:
|
internal: true
|
||||||
# - .:/akkounts
|
|
||||||
# ports:
|
|
||||||
# - "3000:3000"
|
|
||||||
# environment:
|
|
||||||
# RAILS_ENV: development
|
|
||||||
# LDAP_HOST: ldap
|
|
||||||
# LDAP_PORT: 3389
|
|
||||||
# LDAP_ADMIN_PASSWORD: passthebutter
|
|
||||||
# LDAP_USE_TLS: "false"
|
|
||||||
# depends_on:
|
|
||||||
# - ldap
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Remove a potentially pre-existing server.pid for Rails.
|
|
||||||
rm -f /myapp/tmp/pids/server.pid
|
|
||||||
|
|
||||||
# Then exec the container's main process (what's set as CMD in the Dockerfile).
|
|
||||||
exec "$@"
|
|
@ -11,7 +11,7 @@
|
|||||||
"postcss-preset-env": "^7.8.3",
|
"postcss-preset-env": "^7.8.3",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
},
|
},
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
||||||
"build:css": "yarn run build:css:tailwind"
|
"build:css": "yarn run build:css:tailwind"
|
||||||
|
@ -46,5 +46,26 @@ RSpec.describe 'Admin/global settings', type: :feature do
|
|||||||
expect(page).to_not have_checked_field("setting[ejabberd_enabled]")
|
expect(page).to_not have_checked_field("setting[ejabberd_enabled]")
|
||||||
expect(page).to_not have_field("API URL", disabled: true)
|
expect(page).to_not have_field("API URL", disabled: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scenario "View remoteStorage settings" do
|
||||||
|
visit admin_settings_services_path(params: { s: "remotestorage" })
|
||||||
|
|
||||||
|
expect(page).to have_content("Enable RemoteStorage integration")
|
||||||
|
expect(page).to have_field("Storage URL",
|
||||||
|
with: "https://storage.kosmos.org",
|
||||||
|
disabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "Disable remoteStorage integration" do
|
||||||
|
visit admin_settings_services_path(params: { s: "remotestorage" })
|
||||||
|
expect(page).to have_checked_field("setting[remotestorage_enabled]")
|
||||||
|
|
||||||
|
uncheck "setting[remotestorage_enabled]"
|
||||||
|
click_button "Save"
|
||||||
|
|
||||||
|
expect(current_url).to eq(admin_settings_services_url(params: { s: "remotestorage" }))
|
||||||
|
expect(page).to_not have_checked_field("setting[remotestorage_enabled]")
|
||||||
|
expect(page).to_not have_field("Storage URL", disabled: true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -53,11 +53,11 @@ RSpec.describe "Signup", type: :feature do
|
|||||||
expect(page).to have_content("Choose a password")
|
expect(page).to have_content("Choose a password")
|
||||||
|
|
||||||
expect(CreateAccount).to receive(:call)
|
expect(CreateAccount).to receive(:call)
|
||||||
.with(
|
.with({
|
||||||
username: "tony", domain: "kosmos.org",
|
username: "tony", domain: "kosmos.org",
|
||||||
email: "tony@example.com", password: "a-valid-password",
|
email: "tony@example.com", password: "a-valid-password",
|
||||||
invitation: Invitation.last
|
invitation: Invitation.last
|
||||||
).and_return(true)
|
}).and_return(true)
|
||||||
|
|
||||||
fill_in "user_password", with: "a-valid-password"
|
fill_in "user_password", with: "a-valid-password"
|
||||||
click_button "Create account"
|
click_button "Create account"
|
||||||
@ -97,11 +97,11 @@ RSpec.describe "Signup", type: :feature do
|
|||||||
expect(page).to have_content("Password is too short")
|
expect(page).to have_content("Password is too short")
|
||||||
|
|
||||||
expect(CreateAccount).to receive(:call)
|
expect(CreateAccount).to receive(:call)
|
||||||
.with(
|
.with({
|
||||||
username: "tony", domain: "kosmos.org",
|
username: "tony", domain: "kosmos.org",
|
||||||
email: "tony@example.com", password: "a-valid-password",
|
email: "tony@example.com", password: "a-valid-password",
|
||||||
invitation: Invitation.last
|
invitation: Invitation.last
|
||||||
).and_return(true)
|
}).and_return(true)
|
||||||
|
|
||||||
fill_in "user_password", with: "a-valid-password"
|
fill_in "user_password", with: "a-valid-password"
|
||||||
click_button "Create account"
|
click_button "Create account"
|
||||||
|
41
spec/models/user_preferences_spec.rb
Normal file
41
spec/models/user_preferences_spec.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UserPreferences, type: :model do
|
||||||
|
let(:default_prefs) { YAML.load_file("#{Rails.root}/config/default_preferences.yml") }
|
||||||
|
|
||||||
|
describe ".load" do
|
||||||
|
it "provides default values when no preferences are stored yet" do
|
||||||
|
expect(UserPreferences.load(nil)).to eq(default_prefs)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "provides default values for unset preferences" do
|
||||||
|
prefs = UserPreferences.load("lightning_notify_sats_received: xmpp")
|
||||||
|
expect(prefs[:lightning_notify_sats_received]).to eq("xmpp")
|
||||||
|
expect(prefs[:xmpp_exchange_contacts_with_invitees]).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".process" do
|
||||||
|
it "turns all keys into strings" do
|
||||||
|
res = UserPreferences.process({ foo: "bar" })
|
||||||
|
expect(res[:foo]).to be(nil)
|
||||||
|
expect(res['foo']).to eq("bar")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "converts value 'true' to boolean" do
|
||||||
|
res = UserPreferences.process({ lightning_notify_sats_received: "true" })
|
||||||
|
expect(res['lightning_notify_sats_received']).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "converts value 'false' to boolean" do
|
||||||
|
res = UserPreferences.process({ lightning_notify_sats_received: "false" })
|
||||||
|
expect(res['lightning_notify_sats_received']).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "converts value string with integer into integer" do
|
||||||
|
res = UserPreferences.process({ lightning_notify_sats_received_threshold: 1000 })
|
||||||
|
expect(res['lightning_notify_sats_received_threshold']).to be_a(Integer)
|
||||||
|
expect(res['lightning_notify_sats_received_threshold']).to eq(1000)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -109,7 +109,7 @@ RSpec.describe User, type: :model do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
|
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
|
||||||
allow_any_instance_of(User).to receive(:services_enabled).and_return(%w[ ejabberd ])
|
allow_any_instance_of(User).to receive(:services_enabled).and_return(%w[ xmpp ])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "enqueues a job to exchange XMPP contacts between inviter and invitee" do
|
it "enqueues a job to exchange XMPP contacts between inviter and invitee" do
|
||||||
@ -131,14 +131,16 @@ RSpec.describe User, type: :model do
|
|||||||
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
|
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
|
||||||
|
|
||||||
it "enables default services" do
|
it "enables default services" do
|
||||||
expect(user).to receive(:enable_service).with(%w[ discourse ejabberd gitea mediawiki ])
|
expect(user).to receive(:enable_service).with(%w[ discourse gitea mediawiki xmpp ])
|
||||||
user.send(:devise_after_confirmation)
|
user.send(:devise_after_confirmation)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "for invited user with ejabberd enabled" do
|
context "for invited user with xmpp enabled" do
|
||||||
let(:guest) { create :user, id: 2, cn: "isaacnewton", ou: "kosmos.org", email: "newt@example.com" }
|
let(:guest) { create :user, id: 2, cn: "isaacnewton", ou: "kosmos.org", email: "newt@example.com" }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
# TODO remove when defaults are implemented
|
||||||
|
user.update! preferences: { xmpp_exchange_contacts_with_invitees: true }
|
||||||
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
|
Invitation.create! user: user, invited_user_id: guest.id, used_at: DateTime.now
|
||||||
allow_any_instance_of(User).to receive(:enable_service).and_return(true)
|
allow_any_instance_of(User).to receive(:enable_service).and_return(true)
|
||||||
end
|
end
|
||||||
@ -147,6 +149,17 @@ RSpec.describe User, type: :model do
|
|||||||
expect(guest).to receive(:exchange_xmpp_contact_with_inviter)
|
expect(guest).to receive(:exchange_xmpp_contact_with_inviter)
|
||||||
guest.send(:devise_after_confirmation)
|
guest.send(:devise_after_confirmation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "automatic contact exchange disabled" do
|
||||||
|
before do
|
||||||
|
user.update! preferences: { xmpp_exchange_contacts_with_invitees: false }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not exchange XMPP contacts with the inviter" do
|
||||||
|
expect(guest).to_not receive(:exchange_xmpp_contact_with_inviter)
|
||||||
|
guest.send(:devise_after_confirmation)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
49
spec/requests/webfinger_spec.rb
Normal file
49
spec/requests/webfinger_spec.rb
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "WebFinger", type: :request do
|
||||||
|
describe "remoteStorage link relation" do
|
||||||
|
context "user exists" do
|
||||||
|
before do
|
||||||
|
create :user, cn: 'tony', ou: 'kosmos.org'
|
||||||
|
end
|
||||||
|
|
||||||
|
context "remoteStorage enabled globally" do
|
||||||
|
it "includes the remoteStorage link for the user" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
||||||
|
|
||||||
|
expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony@kosmos.org")
|
||||||
|
|
||||||
|
oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"]
|
||||||
|
expect(oauth_url).to eql("https://example.com/rs/oauth")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "remoteStorage not available" do
|
||||||
|
before do
|
||||||
|
Setting.remotestorage_enabled = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not include the remoteStorage link" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Atony%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
res = JSON.parse(response.body)
|
||||||
|
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
||||||
|
|
||||||
|
expect(rs_link).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "user does not exist" do
|
||||||
|
it "does return a 404 status" do
|
||||||
|
get "/.well-known/webfinger?resource=acct%3Ajane.doe%40kosmos.org"
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -55,22 +55,51 @@ RSpec.describe "Webhooks", type: :request do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
user.save! #FIXME this should not be necessary
|
user.save! #FIXME this should not be necessary
|
||||||
post "/webhooks/lndhub", params: payload.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a 200 status" do
|
it "returns a 200 status" do
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "sends an XMPP message to the account owner's JID" do
|
it "does not send notifications by default" do
|
||||||
expect(enqueued_jobs.size).to eq(1)
|
expect(enqueued_jobs.size).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
msg = enqueued_jobs.first['arguments'].first
|
context "notification preference set to 'xmpp'" do
|
||||||
expect(msg["type"]).to eq('normal')
|
before do
|
||||||
expect(msg["from"]).to eq('kosmos.org')
|
user.update! preferences: { lightning_notify_sats_received: "xmpp" }
|
||||||
expect(msg["to"]).to eq(user.address)
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
expect(msg["subject"]).to eq('Sats received!')
|
end
|
||||||
expect(msg["body"]).to match(/^12300 sats received/)
|
|
||||||
|
it "sends an XMPP message to the account owner's JID" do
|
||||||
|
expect(enqueued_jobs.size).to eq(1)
|
||||||
|
expect(enqueued_jobs.first["job_class"]).to eq("XmppSendMessageJob")
|
||||||
|
|
||||||
|
msg = enqueued_jobs.first["arguments"].first
|
||||||
|
expect(msg["type"]).to eq("normal")
|
||||||
|
expect(msg["from"]).to eq("kosmos.org")
|
||||||
|
expect(msg["to"]).to eq(user.address)
|
||||||
|
expect(msg["subject"]).to eq("Sats received!")
|
||||||
|
expect(msg["body"]).to match(/^12,300 sats received/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "notification preference set to 'email'" do
|
||||||
|
before do
|
||||||
|
user.update! preferences: { lightning_notify_sats_received: "email" }
|
||||||
|
post "/webhooks/lndhub", params: payload.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sends an email notification to the account owner" do
|
||||||
|
expect(enqueued_jobs.size).to eq(1)
|
||||||
|
expect(enqueued_jobs.first["job_class"]).to eq("ActionMailer::MailDeliveryJob")
|
||||||
|
args = enqueued_jobs.first['arguments']
|
||||||
|
expect(args[0]).to eq("NotificationMailer")
|
||||||
|
expect(args[1]).to eq("lightning_sats_received")
|
||||||
|
expect(args[3]["params"]["user"]["_aj_globalid"]).to eq("gid://akkounts/User/1")
|
||||||
|
expect(args[3]["params"]["amount_sats"]).to eq(12300)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user