Compare commits
28 Commits
v0.7.0
...
d9b39b36fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9b39b36fb
|
||
|
|
06aed8c33d
|
||
| 0a778e92d8 | |||
|
|
e5a5633e44
|
||
|
|
a68825493f
|
||
|
|
e1e83386a8
|
||
|
|
3adc1917f6
|
||
|
|
8a570ce724
|
||
|
|
c78df9e5f1
|
||
|
5c2df3df07
|
|||
| 83e3e2ecd8 | |||
| b32e2fcb7b | |||
| 96a4db5bae | |||
| c7925f132e | |||
| e4406bf6ff | |||
| ee7769c8c7 | |||
| fdf3218f88 | |||
| 652ed5f7e3 | |||
|
|
e4ed797920
|
||
|
|
93740f17ef
|
||
|
|
affb058671
|
||
|
716d4b944a
|
|||
|
42af148168
|
|||
|
|
dabd892a25
|
||
|
|
eeabbdb7df
|
||
|
ee42d68471
|
|||
|
7acc3b2106
|
|||
|
20c014607c
|
@@ -20,6 +20,7 @@ steps:
|
|||||||
image: guildeducation/rails:2.7.2-14.20.0
|
image: guildeducation/rails:2.7.2-14.20.0
|
||||||
environment:
|
environment:
|
||||||
RAILS_ENV: test
|
RAILS_ENV: test
|
||||||
|
REDIS_URL: redis://redis:6379/0
|
||||||
commands:
|
commands:
|
||||||
- bundle config unset deployment
|
- bundle config unset deployment
|
||||||
- bundle config set cache_all 'true'
|
- bundle config set cache_all 'true'
|
||||||
@@ -42,6 +43,10 @@ steps:
|
|||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: redis
|
||||||
|
image: redis
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
host:
|
host:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
PRIMARY_DOMAIN=kosmos.org
|
PRIMARY_DOMAIN=kosmos.org
|
||||||
|
|
||||||
|
REDIS_URL='redis://localhost:6379/21'
|
||||||
|
|
||||||
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
|
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
|
||||||
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ version-resolver:
|
|||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
- 'release/minor'
|
- 'release/minor'
|
||||||
|
- 'feature'
|
||||||
patch:
|
patch:
|
||||||
labels:
|
labels:
|
||||||
- 'release/patch'
|
- 'release/patch'
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ 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.lock /akkounts/Gemfile.lock
|
COPY ["Gemfile", "Gemfile.lock", "package.json", "./"]
|
||||||
COPY package.json /akkounts/package.json
|
|
||||||
RUN bundle install
|
RUN bundle install
|
||||||
RUN gem install foreman
|
RUN gem install foreman
|
||||||
RUN npm install -g yarn
|
RUN npm install -g yarn
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ GEM
|
|||||||
net-smtp (0.3.3)
|
net-smtp (0.3.3)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
|
nokogiri (1.15.2-arm64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.2-x86_64-linux)
|
nokogiri (1.15.2-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
@@ -373,6 +375,7 @@ GEM
|
|||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
|
sqlite3 (1.6.3-arm64-darwin)
|
||||||
sqlite3 (1.6.3-x86_64-linux)
|
sqlite3 (1.6.3-x86_64-linux)
|
||||||
stimulus-rails (1.2.1)
|
stimulus-rails (1.2.1)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
@@ -410,6 +413,7 @@ GEM
|
|||||||
zeitwerk (2.6.8)
|
zeitwerk (2.6.8)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
arm64-darwin-22
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -14,7 +14,6 @@ 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 `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"`
|
||||||
@@ -53,12 +52,14 @@ Running all specs:
|
|||||||
|
|
||||||
### Docker (Compose)
|
### Docker (Compose)
|
||||||
|
|
||||||
There is a working Docker Compose config file, which allows you to spin up both
|
There is a working Docker Compose config file, which define a number of services including
|
||||||
an app server for Rails as well as a local 389ds (LDAP) server.
|
an app server for Rails as well as a local 389ds (LDAP) server.
|
||||||
|
|
||||||
By default, `docker-compose up` will only start the LDAP server, listening on
|
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
|
||||||
port 389 on your machine. Uncomment other services in `docker-compose.yml` if
|
listening on port 389 on your machine.
|
||||||
you want to use them.
|
|
||||||
|
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
|
||||||
|
the end of the docker compose command. eg. `docker compose up ldap redis`
|
||||||
|
|
||||||
#### LDAP server
|
#### LDAP server
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@import "tailwindcss/components";
|
@import "tailwindcss/components";
|
||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
|
@import "components/animations";
|
||||||
@import "components/base";
|
@import "components/base";
|
||||||
@import "components/buttons";
|
@import "components/buttons";
|
||||||
@import "components/dashboard_services";
|
@import "components/dashboard_services";
|
||||||
|
|||||||
16
app/assets/stylesheets/components/animations.css
Normal file
16
app/assets/stylesheets/components/animations.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-in {
|
||||||
|
animation-name: scaleIn;
|
||||||
|
animation-duration: 0.15s;
|
||||||
|
animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1);
|
||||||
|
}
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
@apply py-1 px-2 text-sm;
|
@apply py-1 px-2 text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline {
|
.btn-icon {
|
||||||
@apply border-2 border-gray-100 hover:bg-gray-100;
|
@apply py-2 px-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-outline {
|
||||||
@apply px-3;
|
@apply py-2 border-2 border-gray-100 hover:bg-gray-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-gray {
|
.btn-gray {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
|
<%= tag.public_send(@tag, class: "mb-6 last:mb-0", data: {
|
||||||
|
:'field-name' => @field_name
|
||||||
|
}) do %>
|
||||||
<% if @positioning == :vertical %>
|
<% 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" %>">
|
||||||
@@ -9,7 +11,21 @@
|
|||||||
<%= @descripton %>
|
<%= @descripton %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= content %>
|
|
||||||
|
<%= tag.p class: "flex gap-x-1", data: {
|
||||||
|
controller: @resettable ? "settings--resettable-field" : nil,
|
||||||
|
} do %>
|
||||||
|
<%= content %>
|
||||||
|
<% if @resettable %>
|
||||||
|
<button type="button"
|
||||||
|
class="relative grow-0 shrink-0 btn-md btn-outline text-red-700"
|
||||||
|
title="Reset to default value"
|
||||||
|
data-settings--resettable-field-target="resetButton"
|
||||||
|
data-action="settings--resettable-field#resetField">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</label>
|
</label>
|
||||||
<% elsif @positioning == :horizontal %>
|
<% elsif @positioning == :horizontal %>
|
||||||
<label class="block flex items-center justify-between">
|
<label class="block flex items-center justify-between">
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
module FormElements
|
module FormElements
|
||||||
class FieldsetComponent < ViewComponent::Base
|
class FieldsetComponent < ViewComponent::Base
|
||||||
def initialize(tag: "li", positioning: :vertical, title:, description: nil)
|
def initialize(tag: "li", positioning: :vertical,
|
||||||
|
title:, description: nil,
|
||||||
|
field_name: nil, resettable: false)
|
||||||
@tag = tag
|
@tag = tag
|
||||||
@positioning = positioning
|
@positioning = positioning
|
||||||
@title = title
|
@title = title
|
||||||
@descripton = description
|
@descripton = description
|
||||||
|
@field_name = field_name
|
||||||
|
@resettable = resettable
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
title: @title,
|
||||||
|
description: @description,
|
||||||
|
field_name: "setting_#{@key.to_s}",
|
||||||
|
resettable: @resettable
|
||||||
|
) do %>
|
||||||
|
<%= method("#{@type}_field").call :setting, @key,
|
||||||
|
value: Setting.public_send(@key),
|
||||||
|
data: {
|
||||||
|
:'default-value' => Setting.get_field(@key)[:default]
|
||||||
|
},
|
||||||
|
class: "w-full" %>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module FormElements
|
||||||
|
class FieldsetResettableSettingComponent < ViewComponent::Base
|
||||||
|
def initialize(tag: "li", key:, type: :text, title:, description: nil)
|
||||||
|
@tag = tag
|
||||||
|
@positioning = :vertical
|
||||||
|
@title = title
|
||||||
|
@descripton = description
|
||||||
|
@key = key.to_sym
|
||||||
|
@type = type
|
||||||
|
@resettable = is_resettable?(@key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_resettable?(key)
|
||||||
|
default_value = Setting.get_field(key)[:default]
|
||||||
|
default_value.present? && (default_value != Setting.send(key))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
15
app/components/modal_component.html.erb
Normal file
15
app/components/modal_component.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div data-modal-target="container"
|
||||||
|
data-action="click->modal#closeBackground keyup@window->modal#closeWithKeyboard"
|
||||||
|
class="hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center"
|
||||||
|
style="z-index: 9999;">
|
||||||
|
<div class="max-h-screen w-auto max-w-lg relative">
|
||||||
|
<div class="m-1 bg-white rounded shadow">
|
||||||
|
<div class="p-8">
|
||||||
|
<%= content %>
|
||||||
|
<div class="flex justify-end items-center flex-wrap mt-6">
|
||||||
|
<button class="btn-md btn-blue" data-action="click->modal#close">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
2
app/components/modal_component.rb
Normal file
2
app/components/modal_component.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class ModalComponent < ViewComponent::Base
|
||||||
|
end
|
||||||
6
app/components/qr_code_modal_component.html.erb
Normal file
6
app/components/qr_code_modal_component.html.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<%= render ModalComponent.new do %>
|
||||||
|
<% if @descripton.present? %>
|
||||||
|
<p class="mb-6"><%= @description %></p>
|
||||||
|
<% end %>
|
||||||
|
<p><%= raw @qr_code_svg %></p>
|
||||||
|
<% end %>
|
||||||
24
app/components/qr_code_modal_component.rb
Normal file
24
app/components/qr_code_modal_component.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
require "rqrcode"
|
||||||
|
|
||||||
|
class QrCodeModalComponent < ViewComponent::Base
|
||||||
|
def initialize(qr_content:, description: nil)
|
||||||
|
@description = description
|
||||||
|
@qr_code_svg = qr_code_svg(qr_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def qr_code_svg(content)
|
||||||
|
qr_code = RQRCode::QRCode.new(content)
|
||||||
|
qr_code.as_svg(
|
||||||
|
color: "000",
|
||||||
|
shape_rendering: "crispEdges",
|
||||||
|
module_size: 6,
|
||||||
|
standalone: true,
|
||||||
|
use_path: true,
|
||||||
|
svg_attributes: {
|
||||||
|
class: 'inline-block'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
130
app/controllers/rs/oauth_controller.rb
Normal file
130
app/controllers/rs/oauth_controller.rb
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
class Rs::OauthController < ApplicationController
|
||||||
|
before_action :require_user_signed_in
|
||||||
|
|
||||||
|
def new
|
||||||
|
username, org = params[:useraddress].split("@")
|
||||||
|
@user = User.where(cn: username.downcase, ou: org).first
|
||||||
|
@scopes = parse_scopes params[:scope]
|
||||||
|
@redirect_uri = params[:redirect_uri]
|
||||||
|
@client_id = params[:client_id]
|
||||||
|
@state = params[:state]
|
||||||
|
@root_access_requested = (@scopes & [":r",":rw"]).any?
|
||||||
|
|
||||||
|
@denial_url = url_with_state("#{@redirect_uri}#error=access_denied", @state)
|
||||||
|
|
||||||
|
@expire_at_dates = [["Never", nil],
|
||||||
|
["In 1 month", 1.month.from_now],
|
||||||
|
["In 1 day", 1.day.from_now]]
|
||||||
|
|
||||||
|
http_status :bad_request and return unless @redirect_uri.present?
|
||||||
|
|
||||||
|
unless current_user == @user
|
||||||
|
sign_out :user
|
||||||
|
|
||||||
|
redirect_to new_rs_oauth_url(@user.address,
|
||||||
|
scope: params[:scope],
|
||||||
|
redirect_uri: params[:redirect_uri],
|
||||||
|
client_id: params[:client_id],
|
||||||
|
state: params[:state])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @client_id.present?
|
||||||
|
redirect_to url_with_state("#{@redirect_uri}#error=invalid_request", @state) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
if @scopes.empty?
|
||||||
|
redirect_to url_with_state("#{@redirect_uri}#error=invalid_scope", @state) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hostname_of(@client_id) == hostname_of(@redirect_uri)
|
||||||
|
redirect_to url_with_state("#{@redirect_uri}#error=invalid_client", @state) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
@client_id.gsub!(/http(s)?:\/\//, "")
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
# if auth = current_user.remote_storage_authorizations.valid.where(permissions: @scopes, client_id: @client_id).first
|
||||||
|
# redirect_to url_with_state("#{@redirect_uri}#access_token=#{auth.token}", @state), allow_other_host: true
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
unless current_user.id.to_s == params[:user_id]
|
||||||
|
Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}")
|
||||||
|
http_status :forbidden and return
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions = parse_scopes params[:scope]
|
||||||
|
redirect_uri = params[:redirect_uri].presence
|
||||||
|
client_id = params[:client_id].presence
|
||||||
|
state = params[:state].presence
|
||||||
|
expire_at = params[:expire_at].presence
|
||||||
|
|
||||||
|
http_status :bad_request and return unless redirect_uri.present?
|
||||||
|
|
||||||
|
if permissions.empty?
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#error=invalid_scope", state), allow_other_host: true and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless client_id.present?
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#error=invalid_request", state), allow_other_host: true and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hostname_of(client_id) == hostname_of(redirect_uri)
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#error=invalid_client", state), allow_other_host: true and return
|
||||||
|
end
|
||||||
|
|
||||||
|
client_id.gsub!(/http(s)?:\/\//, "")
|
||||||
|
|
||||||
|
auth = current_user.remote_storage_authorizations.create!(
|
||||||
|
permissions: permissions,
|
||||||
|
client_id: client_id,
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
app_name: client_id, #TODO use user-defined name
|
||||||
|
expire_at: expire_at
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state), allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /rs/oauth/token/:id/launch_app
|
||||||
|
def launch_app
|
||||||
|
auth = current_user.remote_storage_authorizations.find(params[:id])
|
||||||
|
|
||||||
|
redirect_to app_auth_url(auth)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def app_auth_url(auth)
|
||||||
|
url = "#{auth.url}#remotestorage=#{current_user.address}"
|
||||||
|
url += "&access_token=#{auth.token}"
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
|
def hostname_of(uri)
|
||||||
|
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_scopes(scope_string)
|
||||||
|
return [] if scope_string.blank?
|
||||||
|
|
||||||
|
scopes = scope_string.
|
||||||
|
gsub(/\[|\]/, "").
|
||||||
|
gsub(/\,/, " ").
|
||||||
|
gsub(/\/:/, ":").
|
||||||
|
split(/\s/).map(&:strip).
|
||||||
|
reject(&:empty?)
|
||||||
|
|
||||||
|
scopes = [":r"] if scopes.include?("*:r")
|
||||||
|
scopes = [":rw"] if scopes.include?("*:rw")
|
||||||
|
|
||||||
|
scopes
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_with_state(url, state)
|
||||||
|
state ? "#{url}&state=#{CGI.escape(state)}" : url
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -8,8 +8,7 @@ class Services::LightningController < ApplicationController
|
|||||||
before_action :fetch_balance
|
before_action :fetch_balance
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||||
initialize_lndhub_qr_code
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def transactions
|
def transactions
|
||||||
@@ -56,20 +55,6 @@ class Services::LightningController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def initialize_lndhub_qr_code
|
|
||||||
qr_code = RQRCode::QRCode.new(@wallet_url)
|
|
||||||
@lndhub_qr_svg = qr_code.as_svg(
|
|
||||||
color: "000",
|
|
||||||
shape_rendering: "crispEdges",
|
|
||||||
module_size: 6,
|
|
||||||
standalone: true,
|
|
||||||
use_path: true,
|
|
||||||
svg_attributes: {
|
|
||||||
class: 'inline-block'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_with_lndhub(options={})
|
def authenticate_with_lndhub(options={})
|
||||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||||
@ln_auth_token = session[:ln_auth_token]
|
@ln_auth_token = session[:ln_auth_token]
|
||||||
|
|||||||
11
app/helpers/oauth_helper.rb
Normal file
11
app/helpers/oauth_helper.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module OauthHelper
|
||||||
|
|
||||||
|
def scope_name(scope)
|
||||||
|
scope.gsub(/(\:.+)/, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope_permissions(scope)
|
||||||
|
scope.match(/\:r$/) ? "r" : "rw"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Application } from "@hotwired/stimulus"
|
import { Application } from "@hotwired/stimulus"
|
||||||
|
import { Modal } from "tailwindcss-stimulus-components"
|
||||||
|
|
||||||
const application = Application.start()
|
const application = Application.start()
|
||||||
|
|
||||||
|
application.register('modal', Modal)
|
||||||
|
|
||||||
// Configure Stimulus development experience
|
// Configure Stimulus development experience
|
||||||
application.debug = false
|
application.debug = false
|
||||||
window.Stimulus = application
|
window.Stimulus = application
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "resetButton" ]
|
||||||
|
|
||||||
|
resetField () {
|
||||||
|
const inputEl = this.element.querySelector('input')
|
||||||
|
inputEl.value = inputEl.dataset.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/jobs/expire_remote_storage_authorization_job.rb
Normal file
10
app/jobs/expire_remote_storage_authorization_job.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class ExpireRemoteStorageAuthorizationJob < ApplicationJob
|
||||||
|
queue_as :remote_storage
|
||||||
|
|
||||||
|
def perform(rs_auth_id)
|
||||||
|
rs_auth = RemoteStorageAuthorization.find rs_auth_id
|
||||||
|
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
|
||||||
|
|
||||||
|
rs_auth.destroy!
|
||||||
|
end
|
||||||
|
end
|
||||||
63
app/models/remote_storage_authorization.rb
Normal file
63
app/models/remote_storage_authorization.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
class RemoteStorageAuthorization < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
serialize :permissions
|
||||||
|
|
||||||
|
validates_presence_of :permissions
|
||||||
|
validates_presence_of :client_id
|
||||||
|
|
||||||
|
scope :valid, -> { where(expire_at: nil).or(where(expire_at: (DateTime.now)..)) }
|
||||||
|
scope :expired, -> { where(expire_at: ..(DateTime.now)) }
|
||||||
|
|
||||||
|
after_initialize do |a|
|
||||||
|
a.permisisons = [] if a.permissions == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
before_create :generate_token
|
||||||
|
before_create :store_token_in_redis
|
||||||
|
after_create :schedule_token_expiry
|
||||||
|
before_destroy :delete_token_from_redis
|
||||||
|
after_destroy :remove_token_expiry_job
|
||||||
|
|
||||||
|
def url
|
||||||
|
if self.redirect_uri
|
||||||
|
uri = URI.parse self.redirect_uri
|
||||||
|
"#{uri.scheme}://#{client_id}"
|
||||||
|
else
|
||||||
|
"http://#{client_id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_token_from_redis
|
||||||
|
key = "rs:authorizations:#{user.address}:#{token}"
|
||||||
|
# You can't delete multiple members of a set with Redis 2
|
||||||
|
redis.smembers(key).each { |auth| redis.srem(key, auth) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def redis
|
||||||
|
@redis ||= Redis.new(url: Setting.redis_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_token(length=16)
|
||||||
|
self.token = SecureRandom.hex(length) if self.token.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_token_in_redis
|
||||||
|
redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_token_expiry
|
||||||
|
return unless expire_at.present?
|
||||||
|
ExpireRemoteStorageAuthorizationJob.set(wait_unil: expire_at).perform_later(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_token_expiry_job
|
||||||
|
queue = Sidekiq::Queue.new(ExpireRemoteStorageAuthorizationJob.queue_name)
|
||||||
|
queue.each do |job|
|
||||||
|
next unless job.display_class == "ExpireRemoteStorageAuthorizationJob"
|
||||||
|
job.delete if job.display_args == [id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -29,6 +29,7 @@ class Setting < RailsSettings::Base
|
|||||||
|
|
||||||
field :xmpp_default_rooms, type: :array, default: []
|
field :xmpp_default_rooms, type: :array, default: []
|
||||||
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
|
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
|
||||||
|
field :xmpp_notifications_from_address, type: :string, default: primary_domain
|
||||||
|
|
||||||
#
|
#
|
||||||
# Sentry
|
# Sentry
|
||||||
@@ -41,13 +42,13 @@ class Setting < RailsSettings::Base
|
|||||||
# Discourse
|
# Discourse
|
||||||
#
|
#
|
||||||
|
|
||||||
field :discourse_public_url, type: :string, readonly: true,
|
field :discourse_public_url, type: :string,
|
||||||
default: ENV["DISCOURSE_PUBLIC_URL"].presence
|
default: ENV["DISCOURSE_PUBLIC_URL"].presence
|
||||||
|
|
||||||
field :discourse_enabled, type: :boolean,
|
field :discourse_enabled, type: :boolean,
|
||||||
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
|
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
field :discourse_connect_secret, type: :string, readonly: true,
|
field :discourse_connect_secret, type: :string,
|
||||||
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
|
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -57,10 +58,10 @@ class Setting < RailsSettings::Base
|
|||||||
field :ejabberd_enabled, type: :boolean,
|
field :ejabberd_enabled, type: :boolean,
|
||||||
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
|
default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
|
||||||
|
|
||||||
field :ejabberd_api_url, type: :string, readonly: true,
|
field :ejabberd_api_url, type: :string,
|
||||||
default: ENV["EJABBERD_API_URL"].presence
|
default: ENV["EJABBERD_API_URL"].presence
|
||||||
|
|
||||||
field :ejabberd_admin_url, type: :string, readonly: true,
|
field :ejabberd_admin_url, type: :string,
|
||||||
default: ENV["EJABBERD_ADMIN_URL"].presence
|
default: ENV["EJABBERD_ADMIN_URL"].presence
|
||||||
|
|
||||||
field :ejabberd_buddy_roster, type: :string,
|
field :ejabberd_buddy_roster, type: :string,
|
||||||
@@ -70,7 +71,7 @@ class Setting < RailsSettings::Base
|
|||||||
# Gitea
|
# Gitea
|
||||||
#
|
#
|
||||||
|
|
||||||
field :gitea_public_url, type: :string, readonly: true,
|
field :gitea_public_url, type: :string,
|
||||||
default: ENV["GITEA_PUBLIC_URL"].presence
|
default: ENV["GITEA_PUBLIC_URL"].presence
|
||||||
|
|
||||||
field :gitea_enabled, type: :boolean,
|
field :gitea_enabled, type: :boolean,
|
||||||
@@ -80,7 +81,7 @@ class Setting < RailsSettings::Base
|
|||||||
# Lightning Network
|
# Lightning Network
|
||||||
#
|
#
|
||||||
|
|
||||||
field :lndhub_api_url, type: :string, readonly: true,
|
field :lndhub_api_url, type: :string,
|
||||||
default: ENV["LNDHUB_API_URL"].presence
|
default: ENV["LNDHUB_API_URL"].presence
|
||||||
|
|
||||||
field :lndhub_enabled, type: :boolean,
|
field :lndhub_enabled, type: :boolean,
|
||||||
@@ -89,7 +90,7 @@ class Setting < RailsSettings::Base
|
|||||||
field :lndhub_admin_enabled, type: :boolean,
|
field :lndhub_admin_enabled, type: :boolean,
|
||||||
default: (ENV["LNDHUB_ADMIN_UI"] || false)
|
default: (ENV["LNDHUB_ADMIN_UI"] || false)
|
||||||
|
|
||||||
field :lndhub_public_key, type: :string, readonly: true,
|
field :lndhub_public_key, type: :string,
|
||||||
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
|
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
|
||||||
|
|
||||||
field :lndhub_keysend_enabled, type: :boolean,
|
field :lndhub_keysend_enabled, type: :boolean,
|
||||||
@@ -99,7 +100,7 @@ class Setting < RailsSettings::Base
|
|||||||
# Mastodon
|
# Mastodon
|
||||||
#
|
#
|
||||||
|
|
||||||
field :mastodon_public_url, type: :string, readonly: true,
|
field :mastodon_public_url, type: :string,
|
||||||
default: ENV["MASTODON_PUBLIC_URL"].presence
|
default: ENV["MASTODON_PUBLIC_URL"].presence
|
||||||
|
|
||||||
field :mastodon_enabled, type: :boolean,
|
field :mastodon_enabled, type: :boolean,
|
||||||
@@ -109,7 +110,7 @@ class Setting < RailsSettings::Base
|
|||||||
# MediaWiki
|
# MediaWiki
|
||||||
#
|
#
|
||||||
|
|
||||||
field :mediawiki_public_url, type: :string, readonly: true,
|
field :mediawiki_public_url, type: :string,
|
||||||
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
|
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
|
||||||
|
|
||||||
field :mediawiki_enabled, type: :boolean,
|
field :mediawiki_enabled, type: :boolean,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
has_many :accounts, through: :lndhub_user
|
has_many :accounts, through: :lndhub_user
|
||||||
|
|
||||||
|
has_many :remote_storage_authorizations
|
||||||
|
|
||||||
validates_uniqueness_of :cn, scope: :ou
|
validates_uniqueness_of :cn, scope: :ou
|
||||||
validates_length_of :cn, minimum: 3
|
validates_length_of :cn, minimum: 3
|
||||||
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
||||||
|
|||||||
@@ -8,16 +8,15 @@
|
|||||||
description: "Discourse configuration present and features enabled"
|
description: "Discourse configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.discourse_enabled? %>
|
<% if Setting.discourse_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :discourse_public_url,
|
key: :discourse_public_url,
|
||||||
value: Setting.discourse_public_url,
|
title: "Public URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Connect secret") do %>
|
key: :discourse_connect_secret,
|
||||||
<%= f.password_field :discourse_connect_secret,
|
type: :password,
|
||||||
value: Setting.discourse_connect_secret,
|
title: "Connect secret"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<% if Setting.discourse_enabled? %>
|
<% if Setting.discourse_enabled? %>
|
||||||
@@ -31,14 +30,14 @@
|
|||||||
<input type="text" class="grow" disabled="disabled"
|
<input type="text" class="grow" disabled="disabled"
|
||||||
value="https://<%= Setting.accounts_domain %>/discourse/connect"
|
value="https://<%= Setting.accounts_domain %>/discourse/connect"
|
||||||
data-clipboard-target="source" />
|
data-clipboard-target="source" />
|
||||||
<button class="btn-md btn-icon btn-blue shrink-0"
|
<button class="btn-md btn-icon btn-outline shrink-0"
|
||||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
title="Copy to clipboard">
|
title="Copy to clipboard">
|
||||||
<span class="content-initial">
|
<span class="content-initial">
|
||||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
<span class="content-active hidden">
|
<span class="content-active hidden">
|
||||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -8,16 +8,14 @@
|
|||||||
description: "ejabberd configuration present and features enabled"
|
description: "ejabberd configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.ejabberd_enabled? %>
|
<% if Setting.ejabberd_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :ejabberd_api_url,
|
key: :ejabberd_api_url,
|
||||||
value: Setting.ejabberd_api_url,
|
title: "API URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Admin URL") do %>
|
key: :ejabberd_admin_url,
|
||||||
<%= f.text_field :ejabberd_admin_url,
|
title: "Admin URL"
|
||||||
value: Setting.ejabberd_admin_url,
|
) %>
|
||||||
class: "w-full", disabled: true %>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="mt-10">User default settings</h3>
|
<h3 class="mt-10">User default settings</h3>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
@@ -37,12 +35,24 @@
|
|||||||
title: "Auto-join default rooms",
|
title: "Auto-join default rooms",
|
||||||
description: "Automatically join above default rooms in chat clients"
|
description: "Automatically join above default rooms in chat clients"
|
||||||
) %>
|
) %>
|
||||||
<%= render FormElements::FieldsetComponent.new(
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :ejabberd_buddy_roster,
|
||||||
title: "Contact roster name",
|
title: "Contact roster name",
|
||||||
description: "Used when exchanging contacts after signup from invitation"
|
description: "Used when exchanging contacts after signup from invitation"
|
||||||
|
) %>
|
||||||
|
</ul>
|
||||||
|
<h3 class="mt-10">Notifications</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
|
title: "From address",
|
||||||
|
description: "Address (JID) of the account notifications are sent from",
|
||||||
|
resettable: Setting.get_field(:xmpp_notifications_from_address)[:default] != Setting.xmpp_notifications_from_address
|
||||||
) do %>
|
) do %>
|
||||||
<%= f.text_field :ejabberd_buddy_roster,
|
<%= f.text_field :xmpp_notifications_from_address,
|
||||||
value: Setting.ejabberd_buddy_roster,
|
value: Setting.xmpp_notifications_from_address,
|
||||||
|
data: {
|
||||||
|
:'default-value' => Setting.get_field(:xmpp_notifications_from_address)[:default]
|
||||||
|
},
|
||||||
class: "w-full" %>
|
class: "w-full" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
description: "Gitea configuration present and features enabled"
|
description: "Gitea configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.gitea_enabled? %>
|
<% if Setting.gitea_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :gitea_public_url,
|
key: :gitea_public_url,
|
||||||
value: Setting.gitea_public_url,
|
title: "Public URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||
description: "LNDHub configuration present and wallet features enabled"
|
description: "LNDHub configuration present and wallet features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.lndhub_enabled? %>
|
<% if Setting.lndhub_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :lndhub_api_url,
|
key: :lndhub_api_url,
|
||||||
value: Setting.lndhub_api_url,
|
title: "API URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render FormElements::FieldsetToggleComponent.new(
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
form: f,
|
form: f,
|
||||||
@@ -29,10 +28,10 @@
|
|||||||
description: "Allow users to receive invoice-less payments to their Lightning Address"
|
description: "Allow users to receive invoice-less payments to their Lightning Address"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.lndhub_keysend_enabled? %>
|
<% if Setting.lndhub_keysend_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Public key", description: "The public key of the Lightning node used by LNDHub") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :lndhub_public_key,
|
key: :lndhub_public_key,
|
||||||
value: Setting.lndhub_public_key,
|
title: "Public key",
|
||||||
class: "w-full", disabled: true %>
|
description: "The public key of the Lightning node used by LNDHub"
|
||||||
<% end %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
description: "Mastodon configuration present and features enabled"
|
description: "Mastodon configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.mastodon_enabled? %>
|
<% if Setting.mastodon_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :mastodon_public_url,
|
key: :mastodon_public_url,
|
||||||
value: Setting.mastodon_public_url,
|
title: "Public URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
description: "MediaWiki configuration present and features enabled"
|
description: "MediaWiki configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.mediawiki_enabled? %>
|
<% if Setting.mediawiki_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :mediawiki_public_url,
|
key: :mediawiki_public_url,
|
||||||
value: Setting.mediawiki_public_url,
|
title: "Public URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<h3>RemoteStorage</h3>
|
<h3>RemoteStorage</h3>
|
||||||
|
<p class="text-red-600 mb-8">Feature currently in development.</p>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
<%= render FormElements::FieldsetToggleComponent.new(
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
form: f,
|
form: f,
|
||||||
@@ -8,10 +9,9 @@
|
|||||||
description: "RemoteStorage configuration present and features enabled"
|
description: "RemoteStorage configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.remotestorage_enabled? %>
|
<% if Setting.remotestorage_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Storage URL") do %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
<%= f.text_field :rs_storage_url,
|
key: :rs_storage_url,
|
||||||
value: Setting.rs_storage_url,
|
title: "Storage URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle <%= custom_class %>"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 445 B |
1
app/views/icons/_asterisk.html.erb
Normal file
1
app/views/icons/_asterisk.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512" fill="currentColor" stroke="currentColor" stroke-width="2" class="<%= custom_class %>"><path d="M475.31 364.144L288 256l187.31-108.144c5.74-3.314 7.706-10.653 4.392-16.392l-4-6.928c-3.314-5.74-10.653-7.706-16.392-4.392L272 228.287V12c0-6.627-5.373-12-12-12h-8c-6.627 0-12 5.373-12 12v216.287L52.69 120.144c-5.74-3.314-13.079-1.347-16.392 4.392l-4 6.928c-3.314 5.74-1.347 13.079 4.392 16.392L224 256 36.69 364.144c-5.74 3.314-7.706 10.653-4.392 16.392l4 6.928c3.314 5.74 10.653 7.706 16.392 4.392L240 283.713V500c0 6.627 5.373 12 12 12h8c6.627 0 12-5.373 12-12V283.713l187.31 108.143c5.74 3.314 13.079 1.347 16.392-4.392l4-6.928c3.314-5.74 1.347-13.079-4.392-16.392z"/></svg>
|
||||||
|
After Width: | Height: | Size: 760 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder <%= custom_class %>"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 331 B |
11
app/views/icons/_qr_code.html.erb
Normal file
11
app/views/icons/_qr_code.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg class="icon-qr-code <%= custom_class %>" fill="currentColor" width="90" height="90" version="1.1" viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path id="path2" d="m22.014 22.612c0-2.5389 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0587 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||||
|
<path id="path4" d="m22.014 61.598c0-2.539 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0586 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||||
|
<path id="path6" d="m50 22.612c0-2.5389 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0587 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||||
|
<path id="path8" d="m50 61.598c0-2.539 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0586 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
|
||||||
|
<path id="path10" d="m8.85 45c0-1.7397 1.4103-3.15 3.15-3.15h66.5c1.7397 0 3.15 1.4103 3.15 3.15s-1.4103 3.15-3.15 3.15h-66.5c-1.7397 0-3.15-1.4103-3.15-3.15z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||||
|
<path id="path12" d="m11.566 0c-6.3876 0-11.566 5.1782-11.566 11.566v14.627c0 1.7713 1.4359 3.2073 3.2072 3.2073s3.2072-1.436 3.2072-3.2073v-14.627c0-2.845 2.3064-5.1514 5.1514-5.1514h14.627c1.7713 0 3.2073-1.4359 3.2073-3.2072s-1.436-3.2072-3.2073-3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||||
|
<path id="path14" d="m11.566 90c-6.3876 0-11.566-5.1782-11.566-11.566v-14.628c0-1.7713 1.4359-3.2072 3.2072-3.2072s3.2072 1.4359 3.2072 3.2072v14.628c0 2.845 2.3064 5.1513 5.1514 5.1513h14.627c1.7713 0 3.2073 1.436 3.2073 3.2073 0 1.7712-1.436 3.2072-3.2073 3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||||
|
<path id="path16" d="m78.434 0c6.3876 0 11.566 5.1782 11.566 11.566v14.627c0 1.7713-1.4359 3.2073-3.2072 3.2073s-3.2072-1.436-3.2072-3.2073v-14.627c0-2.845-2.3064-5.1514-5.1514-5.1514h-14.627c-1.7713 0-3.2073-1.4359-3.2073-3.2072s1.436-3.2072 3.2073-3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||||
|
<path id="path18" d="m78.434 90c6.3876 0 11.566-5.1782 11.566-11.566v-14.628c0-1.7713-1.4359-3.2072-3.2072-3.2072s-3.2072 1.4359-3.2072 3.2072v14.628c0 2.845-2.3064 5.1513-5.1514 5.1513h-14.627c-1.7713 0-3.2073 1.436-3.2073 3.2073 0 1.7712 1.436 3.2072 3.2073 3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -8,20 +8,25 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="md:w-3/4">
|
<ul class="md:w-3/4">
|
||||||
<% @invitations_unused.each do |invitation| %>
|
<% @invitations_unused.each do |invitation| %>
|
||||||
<li class="font-mono mb-2 flex gap-1" data-controller="clipboard">
|
<li class="mb-3 flex gap-1" data-controller="clipboard modal">
|
||||||
<input type="text" disabled class="relative grow"
|
<input type="text" disabled class="relative grow font-mono"
|
||||||
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 shrink-0 w-auto"
|
<button class="btn-md btn-icon btn-outline 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">
|
||||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
<span class="content-active hidden">
|
<span class="content-active hidden">
|
||||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||||
|
data-action="click->modal#open" title="Show QR code">
|
||||||
|
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</button>
|
||||||
|
<%= render QrCodeModalComponent.new(qr_content: invitation_url(invitation.token)) %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
58
app/views/rs/oauth/new.html.erb
Normal file
58
app/views/rs/oauth/new.html.erb
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<%= render HeaderCompactComponent.new(title: "Storage") %>
|
||||||
|
|
||||||
|
<%= render MainCompactComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<p class="mb-8">
|
||||||
|
The app on
|
||||||
|
<%= link_to @client_id, "https://#{@client_id}", class: "ks-text-link" %>
|
||||||
|
is asking for access to these folders:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if @root_access_requested %>
|
||||||
|
<p class="text-lg">
|
||||||
|
<span class="text-red-700">
|
||||||
|
<%= render partial: "icons/alert-triangle",
|
||||||
|
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
|
||||||
|
All files and directories
|
||||||
|
</span>
|
||||||
|
<% if (@scopes & [":r"]).any? %>
|
||||||
|
<span class="text-sm text-gray-500">(read only)</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<% @scopes.each do |scope| %>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
<span class="text-lg">
|
||||||
|
<%= render partial: "icons/folder",
|
||||||
|
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
|
||||||
|
<%= scope_name(scope) %>
|
||||||
|
</span>
|
||||||
|
<% if scope_permissions(scope) == "r" %>
|
||||||
|
<span>(read only)</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_with(url: rs_oauth_path, method: :post, data: { turbo: false }) do |f| %>
|
||||||
|
<%= f.hidden_field :redirect_uri, value: @redirect_uri %>
|
||||||
|
<%= f.hidden_field :scope, value: @scopes.join(" ") %>
|
||||||
|
<%= f.hidden_field :user_id, value: @user.id %>
|
||||||
|
<%= f.hidden_field :client_id, value: @client_id %>
|
||||||
|
<%= f.hidden_field :state, value: @state %>
|
||||||
|
<p class="mt-8 mb-6">
|
||||||
|
<%= f.label :expire_at, "Permission expires:", class: "mr-1.5" %>
|
||||||
|
<%= f.select :expire_at, options_for_select(@expire_at_dates) %>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
You can revoke access for this app at any time on your storage dashboard.
|
||||||
|
</p>
|
||||||
|
<p class="mt-8 flex flex-col sm:flex-row gap-3 sm:gap-2 sm:justify-items-stretch">
|
||||||
|
<%= f.submit "Allow",
|
||||||
|
class: "btn-md btn-blue w-full sm:order-last sm:grow",
|
||||||
|
data: { disable_with: "Saving..." } %>
|
||||||
|
<%= link_to "Deny", @denial_url, class: "btn-md btn-gray text-red-700 w-full sm:grow" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
@@ -16,20 +16,20 @@
|
|||||||
<input type="text" id="user_address" class="grow"
|
<input type="text" id="user_address" class="grow"
|
||||||
value=<%= current_user.address %> disabled="disabled"
|
value=<%= current_user.address %> disabled="disabled"
|
||||||
data-clipboard-target="source" />
|
data-clipboard-target="source" />
|
||||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
|
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
title="Copy to clipboard">
|
title="Copy to clipboard">
|
||||||
<span class="content-initial">
|
<span class="content-initial">
|
||||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
<span class="content-active hidden">
|
<span class="content-active hidden">
|
||||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section data-controller="modal">
|
||||||
<h3>Wallet Apps</h3>
|
<h3>Wallet Apps</h3>
|
||||||
<p>
|
<p>
|
||||||
You can connect various wallet apps to your Kosmos account. This allows
|
You can connect various wallet apps to your Kosmos account. This allows
|
||||||
@@ -40,19 +40,16 @@
|
|||||||
</p>
|
</p>
|
||||||
<p data-controller="clipboard" class="my-6 text-center md:text-left">
|
<p data-controller="clipboard" class="my-6 text-center md:text-left">
|
||||||
<input type="text" disabled class="hidden" aria-hidden=true
|
<input type="text" disabled class="hidden" aria-hidden=true
|
||||||
value="<%= @wallet_url%>" data-clipboard-target="source" />
|
value="<%= @wallet_setup_url %>" data-clipboard-target="source" />
|
||||||
<button id="copy-setup-code" class="btn-md btn-blue w-full sm:w-auto"
|
<button id="copy-setup-code" class="btn-md btn-blue w-full sm:w-auto"
|
||||||
data-action="clipboard#copy" data-clipboard-target="trigger">
|
data-action="clipboard#copy" data-clipboard-target="trigger">
|
||||||
<span class="content-initial">Copy setup code/URL</span>
|
<span class="content-initial">Copy setup code/URL</span>
|
||||||
<span class="content-active hidden">Copied ✔</span>
|
<span class="content-active hidden">Copied ✔</span>
|
||||||
</button>
|
</button>
|
||||||
<span class="mx-2 my-2 md:my-0 block md:inline">or</span>
|
<span class="mx-2 my-2 md:my-0 block md:inline">or</span>
|
||||||
<button id="show-setup-code" class="btn-md btn-blue w-full sm:w-auto">Show setup QR code</button>
|
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">Show setup QR code</button>
|
||||||
<button id="hide-setup-code" class="hidden btn-md btn-blue w-full sm:w-auto">Hide setup QR code</button>
|
|
||||||
</p>
|
|
||||||
<p id="setup-code" class="hidden my-10 w-full text-center">
|
|
||||||
<%= raw @lndhub_qr_svg %>
|
|
||||||
</p>
|
</p>
|
||||||
|
<%= render QrCodeModalComponent.new(qr_content: @wallet_setup_url) %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -119,27 +116,3 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function () {
|
|
||||||
const buttonShow = document.querySelector('#show-setup-code');
|
|
||||||
const buttonHide = document.querySelector('#hide-setup-code');
|
|
||||||
const setupCode = document.querySelector('#setup-code');
|
|
||||||
|
|
||||||
buttonShow.addEventListener('click', function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
setupCode.classList.remove('hidden');
|
|
||||||
buttonHide.classList.remove('hidden');
|
|
||||||
buttonShow.classList.add('hidden');
|
|
||||||
setupCode.scrollIntoView({behavior: "smooth", block: "nearest"});
|
|
||||||
});
|
|
||||||
|
|
||||||
buttonHide.addEventListener('click', function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
const el = document.querySelector('#setup-code');
|
|
||||||
setupCode.classList.add('hidden');
|
|
||||||
buttonHide.classList.add('hidden');
|
|
||||||
buttonShow.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
'settings--account--email-target': 'emailField'
|
'settings--account--email-target': 'emailField'
|
||||||
}, required: true %>
|
}, required: true %>
|
||||||
<button type="button" id="edit-email"
|
<button type="button" id="edit-email"
|
||||||
class="btn-md btn-icon btn-blue shrink-0 hidden initial-visible"
|
class="btn-md btn-icon btn-outline shrink-0 hidden initial-visible"
|
||||||
data-settings--account--email-target="editEmailButton"
|
data-settings--account--email-target="editEmailButton"
|
||||||
data-action="settings--account--email#editEmail"
|
data-action="settings--account--email#editEmail"
|
||||||
title="Edit email address">
|
title="Edit email address">
|
||||||
<span class="">
|
<span class="">
|
||||||
<%= render partial: "icons/edit-3", locals: {
|
<%= render partial: "icons/edit-3", locals: {
|
||||||
custom_class: "text-white h-4 w-4 inline" } %>
|
custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
<input type="text" id="user_address" class="grow"
|
<input type="text" id="user_address" class="grow"
|
||||||
value=<%= @user.address %> disabled="disabled"
|
value=<%= @user.address %> disabled="disabled"
|
||||||
data-clipboard-target="source" />
|
data-clipboard-target="source" />
|
||||||
<button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
|
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||||
data-clipboard-target="trigger" data-action="clipboard#copy"
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
title="Copy to clipboard">
|
title="Copy to clipboard">
|
||||||
<span class="content-initial">
|
<span class="content-initial">
|
||||||
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
<span class="content-active hidden">
|
<span class="content-active hidden">
|
||||||
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
|||||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
||||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||||
pin "bech32" # @2.0.0
|
pin "bech32" # @2.0.0
|
||||||
|
pin "tailwindcss-stimulus-components" # @3.0.4
|
||||||
|
|||||||
@@ -66,7 +66,13 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get ".well-known/webfinger", to: 'webfinger#show'
|
namespace :rs do
|
||||||
|
resource :oauth, only: [:new, :create], path_names: { new: ':useraddress' },
|
||||||
|
controller: 'oauth', constraints: { useraddress: /[^\/]+/}
|
||||||
|
get 'oauth/token/:id/launch_app' => 'oauth#launch_app', as: :launch_app
|
||||||
|
end
|
||||||
|
|
||||||
|
get '.well-known/webfinger', to: 'webfinger#show'
|
||||||
|
|
||||||
namespace :discourse do
|
namespace :discourse do
|
||||||
get "connect", to: 'sso#connect'
|
get "connect", to: 'sso#connect'
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class CreateRemoteStorageAuthorizations < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :remote_storage_authorizations do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.string :token
|
||||||
|
t.text :permissions, array: true, default: [].to_yaml
|
||||||
|
t.string :client_id
|
||||||
|
t.string :redirect_uri
|
||||||
|
t.string :app_name
|
||||||
|
t.datetime :expire_at
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :remote_storage_authorizations, :permissions, using: 'gin'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -87,10 +87,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) 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.datetime "remember_created_at"
|
||||||
t.string "remember_token"
|
t.string "remember_token"
|
||||||
t.text "preferences"
|
t.text "preferences"
|
||||||
|
t.string "nostr_pubkey"
|
||||||
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
|
||||||
|
|||||||
@@ -12,57 +12,60 @@ services:
|
|||||||
DS_DM_PASSWORD: passthebutter
|
DS_DM_PASSWORD: passthebutter
|
||||||
SUFFIX_NAME: "dc=kosmos,dc=org"
|
SUFFIX_NAME: "dc=kosmos,dc=org"
|
||||||
|
|
||||||
# redis:
|
redis:
|
||||||
# restart: always
|
restart: always
|
||||||
# image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
# networks:
|
networks:
|
||||||
# - internal_network
|
- internal_network
|
||||||
# healthcheck:
|
healthcheck:
|
||||||
# test: ['CMD', 'redis-cli', 'ping']
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
# volumes:
|
volumes:
|
||||||
# - ./tmp/redis:/data
|
- ./tmp/redis:/data
|
||||||
|
|
||||||
# web:
|
web:
|
||||||
# build: .
|
build: .
|
||||||
# tty: true
|
tty: true
|
||||||
# command: bash -c "rm -f /akkounts/tmp/pids/server.pid; bin/dev"
|
command: bash -c "rm -f /akkounts/tmp/pids/server.pid; bin/dev"
|
||||||
# volumes:
|
volumes:
|
||||||
# - .:/akkounts
|
- .:/akkounts
|
||||||
# networks:
|
- /akkounts/node_modules
|
||||||
# - external_network
|
networks:
|
||||||
# - internal_network
|
- external_network
|
||||||
# ports:
|
- internal_network
|
||||||
# - "3000:3000"
|
ports:
|
||||||
# environment:
|
- "3000:3000"
|
||||||
# RAILS_ENV: development
|
environment:
|
||||||
# REDIS_URL: redis://redis:6379/0
|
RAILS_ENV: development
|
||||||
# LDAP_HOST: ldap
|
PRIMARY_DOMAIN: kosmos.org
|
||||||
# LDAP_PORT: 3389
|
REDIS_URL: redis://redis:6379/0
|
||||||
# LDAP_ADMIN_PASSWORD: passthebutter
|
LDAP_HOST: ldap
|
||||||
# LDAP_USE_TLS: "false"
|
LDAP_PORT: 3389
|
||||||
# depends_on:
|
LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
# - ldap
|
LDAP_USE_TLS: "false"
|
||||||
# - redis
|
depends_on:
|
||||||
|
- ldap
|
||||||
|
- redis
|
||||||
|
|
||||||
# sidekiq:
|
sidekiq:
|
||||||
# build: .
|
build: .
|
||||||
# command: bash -c "bundle exec sidekiq -C config/sidekiq.yml"
|
command: bash -c "bundle exec sidekiq -C config/sidekiq.yml"
|
||||||
# volumes:
|
volumes:
|
||||||
# - .:/akkounts
|
- .:/akkounts
|
||||||
# networks:
|
networks:
|
||||||
# - internal_network
|
- internal_network
|
||||||
# environment:
|
environment:
|
||||||
# RAILS_ENV: development
|
RAILS_ENV: development
|
||||||
# REDIS_URL: redis://redis:6379/0
|
PRIMARY_DOMAIN: kosmos.org
|
||||||
# LDAP_HOST: ldap
|
REDIS_URL: redis://redis:6379/0
|
||||||
# LDAP_PORT: 3389
|
LDAP_HOST: ldap
|
||||||
# LDAP_ADMIN_PASSWORD: passthebutter
|
LDAP_PORT: 3389
|
||||||
# LDAP_USE_TLS: "false"
|
LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
# LAUNCHY_DRY_RUN: true
|
LDAP_USE_TLS: "false"
|
||||||
# BROWSER: /dev/null
|
LAUNCHY_DRY_RUN: true
|
||||||
# depends_on:
|
BROWSER: /dev/null
|
||||||
# - ldap
|
depends_on:
|
||||||
# - redis
|
- ldap
|
||||||
|
- redis
|
||||||
|
|
||||||
# phpldapadmin:
|
# phpldapadmin:
|
||||||
# image: osixia/phpldapadmin:0.9.0
|
# image: osixia/phpldapadmin:0.9.0
|
||||||
|
|||||||
9
spec/factories/remote_storage_authorizations.rb
Normal file
9
spec/factories/remote_storage_authorizations.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :remote_storage_authorization do
|
||||||
|
permissions { ["documents:rw"] }
|
||||||
|
client_id { "some-fancy-app" }
|
||||||
|
redirect_uri { "https://example.com/some-fancy-app" }
|
||||||
|
app_name { "Fancy App" }
|
||||||
|
expire_at { nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -26,16 +26,14 @@ RSpec.describe 'Admin/global settings', type: :feature do
|
|||||||
expect(current_url).to eq(admin_settings_services_url(params: { s: "discourse" }))
|
expect(current_url).to eq(admin_settings_services_url(params: { s: "discourse" }))
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "View ejabberd settings" do
|
scenario "View service settings" do
|
||||||
visit admin_settings_services_path(params: { s: "ejabberd" })
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
|
|
||||||
expect(page).to have_content("Enable ejabberd integration")
|
expect(page).to have_content("Enable ejabberd integration")
|
||||||
expect(page).to have_field("API URL",
|
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
||||||
with: "http://xmpp.example.com/api",
|
|
||||||
disabled: true)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "Disable ejabberd integration" do
|
scenario "Disable a service integration" do
|
||||||
visit admin_settings_services_path(params: { s: "ejabberd" })
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
expect(page).to have_checked_field("setting[ejabberd_enabled]")
|
expect(page).to have_checked_field("setting[ejabberd_enabled]")
|
||||||
|
|
||||||
@@ -47,25 +45,15 @@ RSpec.describe 'Admin/global settings', type: :feature do
|
|||||||
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
|
scenario "Resettable fields" do
|
||||||
visit admin_settings_services_path(params: { s: "remotestorage" })
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
|
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
||||||
|
expect(page).to_not have_css('input#setting_ejabberd_api_url+button')
|
||||||
|
|
||||||
expect(page).to have_content("Enable RemoteStorage integration")
|
Setting.ejabberd_api_url = "http://example.com/foo"
|
||||||
expect(page).to have_field("Storage URL",
|
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||||
with: "https://storage.kosmos.org",
|
expect(page).to have_field("API URL", with: "http://example.com/foo")
|
||||||
disabled: true)
|
expect(page).to have_css('input#setting_ejabberd_api_url+button')
|
||||||
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
|
end
|
||||||
|
|||||||
36
spec/jobs/expire_remote_storage_authorization_job_spec.rb
Normal file
36
spec/jobs/expire_remote_storage_authorization_job_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ExpireRemoteStorageAuthorizationJob, type: :job do
|
||||||
|
before do
|
||||||
|
@user = create :user, cn: "ronald", ou: "kosmos.org"
|
||||||
|
@rs_authorization = create :remote_storage_authorization, user: @user, expire_at: 1.day.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
clear_enqueued_jobs
|
||||||
|
clear_performed_jobs
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:job) {
|
||||||
|
described_class.perform_later(@rs_authorization.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:redis) {
|
||||||
|
@redis ||= Redis.new(url: Setting.redis_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "removes the RS authorization from redis" do
|
||||||
|
redis_key = "rs:authorizations:#{@user.address}:#{@rs_authorization.token}"
|
||||||
|
expect(redis.keys(redis_key)).to_not be_empty
|
||||||
|
|
||||||
|
perform_enqueued_jobs { job }
|
||||||
|
|
||||||
|
expect(redis.keys(redis_key)).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes the RS authorization object" do
|
||||||
|
expect {
|
||||||
|
perform_enqueued_jobs { job }
|
||||||
|
}.to change(RemoteStorageAuthorization, :count).by(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,7 +5,8 @@ module.exports = {
|
|||||||
'./app/components/**/*.rb',
|
'./app/components/**/*.rb',
|
||||||
'./app/views/**/*.html.erb',
|
'./app/views/**/*.html.erb',
|
||||||
'./app/helpers/**/*.rb',
|
'./app/helpers/**/*.rb',
|
||||||
'./app/javascript/**/*.js'
|
'./app/javascript/**/*.js',
|
||||||
|
'./vendor/javascript/tailwindcss-stimulus-components.js'
|
||||||
],
|
],
|
||||||
safelist: [
|
safelist: [
|
||||||
'bg-gray-100', 'text-gray-800',
|
'bg-gray-100', 'text-gray-800',
|
||||||
|
|||||||
2
vendor/javascript/tailwindcss-stimulus-components.js
vendored
Normal file
2
vendor/javascript/tailwindcss-stimulus-components.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user