Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6739b38f4c
|
||
| 7e1272c936 | |||
|
|
ecdeb4c122
|
||
|
|
8614e2f12b
|
||
|
|
a038a857d9
|
||
|
|
eee81d0cf1
|
||
|
|
b7fa4b012a
|
||
|
|
10bcd5c32b
|
||
|
|
f79d5d4724
|
||
|
|
866ffbe615
|
||
|
|
3c1fe3396d
|
||
|
|
e4242333d9
|
||
|
|
138f13c1a0
|
||
|
|
ad5e515200
|
||
|
|
1ea8b22a59
|
||
|
|
f49aff262c
|
||
| 852e2fea1e | |||
|
|
353b55fe1a
|
||
|
|
ba0cbba96b
|
||
|
|
5f921f1b53
|
||
|
|
a2d27bf575
|
||
|
|
fcf9a065e1
|
||
|
|
ec9bcacd46
|
||
|
|
645abac810 | ||
|
|
e11be727a1 | ||
|
|
12b24337e7 | ||
|
|
b0bfc290c4 | ||
|
|
4c6c81171b | ||
|
|
4d88a40109
|
||
|
|
d9b39b36fb
|
||
|
|
06aed8c33d
|
||
| 0a778e92d8 | |||
|
|
e5a5633e44
|
||
|
|
a68825493f
|
||
|
|
e1e83386a8
|
||
|
|
3adc1917f6
|
||
|
|
8a570ce724
|
||
|
|
c78df9e5f1
|
||
|
5c2df3df07
|
|||
| 83e3e2ecd8 | |||
| b32e2fcb7b | |||
| 96a4db5bae | |||
| c7925f132e | |||
| e4406bf6ff | |||
| ee7769c8c7 | |||
| fdf3218f88 | |||
| 652ed5f7e3 | |||
|
|
e4ed797920
|
||
|
|
93740f17ef
|
||
|
|
affb058671
|
||
|
716d4b944a
|
|||
|
42af148168
|
|||
|
|
dabd892a25
|
||
|
|
eeabbdb7df
|
||
|
ee42d68471
|
|||
|
7acc3b2106
|
|||
|
20c014607c
|
@@ -20,6 +20,8 @@ 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
|
||||||
|
RS_REDIS_URL: redis://redis:6379/1
|
||||||
commands:
|
commands:
|
||||||
- bundle config unset deployment
|
- bundle config unset deployment
|
||||||
- bundle config set cache_all 'true'
|
- bundle config set cache_all 'true'
|
||||||
@@ -42,6 +44,10 @@ steps:
|
|||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: redis
|
||||||
|
image: redis
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: cache
|
- name: cache
|
||||||
host:
|
host:
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
|||||||
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||||
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||||
|
|
||||||
|
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
||||||
|
|
||||||
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||||
MASTODON_PUBLIC_URL='https://kosmos.social'
|
MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||||
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||||
|
RS_REDIS_URL='redis://localhost:6379/2'
|
||||||
|
|
||||||
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||||
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
PRIMARY_DOMAIN=kosmos.org
|
PRIMARY_DOMAIN=kosmos.org
|
||||||
|
|
||||||
|
REDIS_URL='redis://localhost:6379/0'
|
||||||
|
|
||||||
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'
|
||||||
|
|
||||||
@@ -12,5 +14,6 @@ LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
|||||||
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||||
|
|
||||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||||
|
RS_REDIS_URL='redis://localhost:6379/1'
|
||||||
|
|
||||||
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
|
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ version-resolver:
|
|||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
- 'release/minor'
|
- 'release/minor'
|
||||||
|
- 'feature'
|
||||||
patch:
|
patch:
|
||||||
labels:
|
labels:
|
||||||
- 'release/patch'
|
- 'release/patch'
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -39,6 +39,7 @@ yarn-debug.log*
|
|||||||
|
|
||||||
# Ignore local dotenv config file
|
# Ignore local dotenv config file
|
||||||
.env
|
.env
|
||||||
|
.env.development
|
||||||
|
|
||||||
# Ignore redis dumps from sidekiq
|
# Ignore redis dumps from sidekiq
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
Gemfile
@@ -64,6 +64,7 @@ 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'
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
|
gem 'rails-controller-testing'
|
||||||
gem "byebug", "~> 11.1"
|
gem "byebug", "~> 11.1"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -267,6 +269,10 @@ GEM
|
|||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.5)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.5)
|
railties (= 7.0.5)
|
||||||
|
rails-controller-testing (1.0.5)
|
||||||
|
actionpack (>= 5.0.1.rc1)
|
||||||
|
actionview (>= 5.0.1.rc1)
|
||||||
|
activesupport (>= 5.0.1.rc1)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
@@ -373,6 +379,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 +417,7 @@ GEM
|
|||||||
zeitwerk (2.6.8)
|
zeitwerk (2.6.8)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
arm64-darwin-22
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
@@ -440,6 +448,7 @@ DEPENDENCIES
|
|||||||
pg (~> 1.2.3)
|
pg (~> 1.2.3)
|
||||||
puma (~> 4.1)
|
puma (~> 4.1)
|
||||||
rails (~> 7.0.2)
|
rails (~> 7.0.2)
|
||||||
|
rails-controller-testing
|
||||||
rails-settings-cached (~> 2.8.3)
|
rails-settings-cached (~> 2.8.3)
|
||||||
rqrcode (~> 2.0)
|
rqrcode (~> 2.0)
|
||||||
rspec-rails
|
rspec-rails
|
||||||
|
|||||||
12
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
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ command:
|
|||||||
* [Tailwind CSS](https://tailwindcss.com/)
|
* [Tailwind CSS](https://tailwindcss.com/)
|
||||||
* [Sass](https://sass-lang.com/documentation)
|
* [Sass](https://sass-lang.com/documentation)
|
||||||
* [Stimulus](https://stimulus.hotwired.dev/handbook/)
|
* [Stimulus](https://stimulus.hotwired.dev/handbook/)
|
||||||
|
* [Tailwind Stimulus Components](https://github.com/excid3/tailwindcss-stimulus-components)
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
15
app/components/app_info_component.html.erb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="flex">
|
||||||
|
<div class="<%= @icon_container_class %>">
|
||||||
|
<%= image_tag(@icon_path, class: 'h-full w-full') %>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 px-4">
|
||||||
|
<h4 class="sm:pt-2 mb-2 text-lg font-bold"><%= @name %></h4>
|
||||||
|
<p class="leading-snug"><%= @description %></p>
|
||||||
|
<p class="leading-snug flex flex-wrap gap-3">
|
||||||
|
<% @links.each do |link| %>
|
||||||
|
<a href="<%= link[1] %>" target="_blank"
|
||||||
|
class="flex-0 btn-sm btn-gray"><%= link[0] %></a>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
19
app/components/app_info_component.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AppInfoComponent < ViewComponent::Base
|
||||||
|
def initialize(name:, description:, icon_path: , icon_fill_box: false, links: [])
|
||||||
|
@name = name
|
||||||
|
@description = description
|
||||||
|
@icon_path = icon_path
|
||||||
|
@icon_container_class = icon_container_class(icon_fill_box)
|
||||||
|
@links = links
|
||||||
|
end
|
||||||
|
|
||||||
|
def icon_container_class(icon_fill_box)
|
||||||
|
str = "flex-0 h-16 w-16 sm:h-28 sm:w-28 bg-white rounded-3xl overflow-hidden"
|
||||||
|
unless icon_fill_box
|
||||||
|
str += " p-2 border border-gray-200"
|
||||||
|
end
|
||||||
|
str
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
28
app/components/modal_component.html.erb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<div tabindex="-1" class="relative z-10">
|
||||||
|
<!-- Modal Background -->
|
||||||
|
<div class="hidden fixed inset-0 bg-black bg-opacity-80 overflow-y-auto flex items-center justify-center"
|
||||||
|
data-modal-target="background"
|
||||||
|
data-action="click->modal#closeBackground"
|
||||||
|
data-transition-enter="transition-all ease-in-out duration-100"
|
||||||
|
data-transition-enter-from="bg-opacity-0"
|
||||||
|
data-transition-enter-to="bg-opacity-80"
|
||||||
|
data-transition-leave="transition-all ease-in-out duration-100"
|
||||||
|
data-transition-leave-from="bg-opacity-80"
|
||||||
|
data-transition-leave-to="bg-opacity-0">
|
||||||
|
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div data-modal-target="container"
|
||||||
|
class="max-h-screen w-auto max-w-lg relative
|
||||||
|
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||||
|
<!-- Modal Card -->
|
||||||
|
<div class="m-1 bg-white rounded shadow">
|
||||||
|
<div class="p-8">
|
||||||
|
<%= content %>
|
||||||
|
<div class="flex justify-end items-center flex-wrap mt-6">
|
||||||
|
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
2
app/components/modal_component.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class ModalComponent < ViewComponent::Base
|
||||||
|
end
|
||||||
6
app/components/qr_code_modal_component.html.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<%= render ModalComponent.new do %>
|
||||||
|
<% if @descripton.present? %>
|
||||||
|
<p class="mb-6"><%= @description %></p>
|
||||||
|
<% end %>
|
||||||
|
<p><%= raw @qr_code_svg %></p>
|
||||||
|
<% end %>
|
||||||
24
app/components/qr_code_modal_component.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
require "rqrcode"
|
||||||
|
|
||||||
|
class QrCodeModalComponent < ViewComponent::Base
|
||||||
|
def initialize(qr_content:, description: nil)
|
||||||
|
@description = description
|
||||||
|
@qr_code_svg = qr_code_svg(qr_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def qr_code_svg(content)
|
||||||
|
qr_code = RQRCode::QRCode.new(content)
|
||||||
|
qr_code.as_svg(
|
||||||
|
color: "000",
|
||||||
|
shape_rendering: "crispEdges",
|
||||||
|
module_size: 6,
|
||||||
|
standalone: true,
|
||||||
|
use_path: true,
|
||||||
|
svg_attributes: {
|
||||||
|
class: 'inline-block'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
145
app/controllers/rs/oauth_controller.rb
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
class Rs::OauthController < ApplicationController
|
||||||
|
before_action :require_signed_in_with_username, only: :new
|
||||||
|
before_action :authenticate_user!, only: :create
|
||||||
|
|
||||||
|
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),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
if @scopes.empty?
|
||||||
|
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_scope", @state),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hostname_of(@client_id) == hostname_of(@redirect_uri)
|
||||||
|
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_client", @state),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
@client_id.gsub!(/http(s)?:\/\//, "")
|
||||||
|
|
||||||
|
if auth = current_user.remote_storage_authorizations.valid.where(permissions: @scopes, client_id: @client_id).first
|
||||||
|
redirect_to(url_with_state("#{@redirect_uri}#access_token=#{auth.token}", @state),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
unless current_user.id.to_s == params[:user_id]
|
||||||
|
Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}")
|
||||||
|
http_status :forbidden and return
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions = parse_scopes params[:scope]
|
||||||
|
redirect_uri = params[:redirect_uri].presence
|
||||||
|
client_id = params[:client_id].presence
|
||||||
|
state = params[:state].presence
|
||||||
|
expire_at = params[:expire_at].presence
|
||||||
|
|
||||||
|
http_status :bad_request and return unless redirect_uri.present?
|
||||||
|
|
||||||
|
if permissions.empty?
|
||||||
|
redirect_to(url_with_state("#{redirect_uri}#error=invalid_scope", state),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless client_id.present?
|
||||||
|
redirect_to(url_with_state("#{redirect_uri}#error=invalid_request", state),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hostname_of(client_id) == hostname_of(redirect_uri)
|
||||||
|
redirect_to(url_with_state("#{redirect_uri}#error=invalid_client", state),
|
||||||
|
allow_other_host: true) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
client_id.gsub!(/http(s)?:\/\//, "")
|
||||||
|
|
||||||
|
auth = current_user.remote_storage_authorizations.create!(
|
||||||
|
permissions: permissions,
|
||||||
|
client_id: client_id,
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
app_name: client_id, #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), allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_signed_in_with_username
|
||||||
|
unless user_signed_in?
|
||||||
|
username, org = params[:useraddress].split("@")
|
||||||
|
redirect_to new_user_session_path(cn: username, ou: org)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_auth_url(auth)
|
||||||
|
url = "#{auth.url}#remotestorage=#{current_user.address}"
|
||||||
|
url += "&access_token=#{auth.token}"
|
||||||
|
url
|
||||||
|
end
|
||||||
|
|
||||||
|
def hostname_of(uri)
|
||||||
|
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_scopes(scope_string)
|
||||||
|
return [] if scope_string.blank?
|
||||||
|
|
||||||
|
scopes = scope_string.
|
||||||
|
gsub(/\[|\]/, "").
|
||||||
|
gsub(/\,/, " ").
|
||||||
|
gsub(/\/:/, ":").
|
||||||
|
split(/\s/).map(&:strip).
|
||||||
|
reject(&:empty?)
|
||||||
|
|
||||||
|
scopes = [":r"] if scopes.include?("*:r")
|
||||||
|
scopes = [":rw"] if scopes.include?("*:rw")
|
||||||
|
|
||||||
|
scopes
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_with_state(url, state)
|
||||||
|
state ? "#{url}&state=#{CGI.escape(state)}" : url
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
9
app/controllers/services/base_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Services::BaseController < ApplicationController
|
||||||
|
before_action :set_current_section
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_section
|
||||||
|
@current_section = :services
|
||||||
|
end
|
||||||
|
end
|
||||||
14
app/controllers/services/chat_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class Services::ChatController < Services::BaseController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :require_service_available
|
||||||
|
|
||||||
|
def show
|
||||||
|
@service_enabled = current_user.services_enabled.include?(:xmpp)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_service_available
|
||||||
|
http_status :not_found unless Setting.ejabberd_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,7 @@ class Services::LightningController < ApplicationController
|
|||||||
before_action :fetch_balance
|
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]
|
||||||
|
|||||||
14
app/controllers/services/mastodon_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class Services::MastodonController < Services::BaseController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :require_service_available
|
||||||
|
|
||||||
|
def show
|
||||||
|
@service_enabled = current_user.services_enabled.include?(:mastodon)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_service_available
|
||||||
|
http_status :not_found unless Setting.mastodon_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
class Services::RemotestorageController < ApplicationController
|
class Services::RemotestorageController < Services::BaseController
|
||||||
before_action :require_user_signed_in
|
before_action :authenticate_user!
|
||||||
before_action :require_service_enabled
|
|
||||||
before_action :require_feature_enabled
|
before_action :require_feature_enabled
|
||||||
before_action :set_current_section
|
before_action :require_service_available
|
||||||
|
|
||||||
def dashboard
|
def dashboard
|
||||||
# unless current_user.services_enabled.include?(:remotestorage)
|
# unless current_user.services_enabled.include?(:remotestorage)
|
||||||
@@ -18,13 +17,7 @@ class Services::RemotestorageController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_service_enabled
|
def require_service_available
|
||||||
unless Setting.remotestorage_enabled?
|
http_status :not_found unless Setting.remotestorage_enabled?
|
||||||
http_status :not_found
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_current_section
|
|
||||||
@current_section = :services
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
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,11 @@
|
|||||||
import { Application } from "@hotwired/stimulus"
|
import { Application } from "@hotwired/stimulus"
|
||||||
|
import { Modal, Tabs } from "tailwindcss-stimulus-components"
|
||||||
|
|
||||||
const application = Application.start()
|
const application = Application.start()
|
||||||
|
|
||||||
|
application.register('modal', Modal)
|
||||||
|
application.register('tabs', Tabs)
|
||||||
|
|
||||||
// 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/remote_storage_expire_authorization_job.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class RemoteStorageExpireAuthorizationJob < ApplicationJob
|
||||||
|
queue_as :remotestorage
|
||||||
|
|
||||||
|
def perform(rs_auth_id)
|
||||||
|
rs_auth = RemoteStorageAuthorization.find rs_auth_id
|
||||||
|
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
|
||||||
|
|
||||||
|
rs_auth.destroy!
|
||||||
|
end
|
||||||
|
end
|
||||||
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.permissions = [] 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}"
|
||||||
|
redis.srem? key, redis.smembers(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def redis
|
||||||
|
@redis ||= Redis.new(url: Setting.rs_redis_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_token(length=16)
|
||||||
|
self.token = SecureRandom.hex(length) if self.token.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_token_in_redis
|
||||||
|
redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_token_expiry
|
||||||
|
return unless expire_at.present?
|
||||||
|
RemoteStorageExpireAuthorizationJob.set(wait_until: expire_at)
|
||||||
|
.perform_later(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_token_expiry_job
|
||||||
|
queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
|
||||||
|
queue.each do |job|
|
||||||
|
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
|
||||||
|
job.delete if job.display_args == [id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,7 +12,7 @@ class Setting < RailsSettings::Base
|
|||||||
# Internal services
|
# Internal services
|
||||||
#
|
#
|
||||||
|
|
||||||
field :redis_url, type: :string, readonly: true,
|
field :redis_url, type: :string,
|
||||||
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -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,15 +42,25 @@ 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
|
||||||
|
|
||||||
|
#
|
||||||
|
# Drone CI
|
||||||
|
#
|
||||||
|
|
||||||
|
field :droneci_public_url, type: :string,
|
||||||
|
default: ENV["DRONECI_PUBLIC_URL"].presence
|
||||||
|
|
||||||
|
field :droneci_enabled, type: :boolean,
|
||||||
|
default: (ENV["DRONECI_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
#
|
#
|
||||||
# ejabberd
|
# ejabberd
|
||||||
#
|
#
|
||||||
@@ -57,10 +68,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 +81,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 +91,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 +100,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,17 +110,20 @@ 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,
|
||||||
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
|
default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
field :mastodon_address_domain, type: :string,
|
||||||
|
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
|
||||||
|
|
||||||
#
|
#
|
||||||
# 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,
|
||||||
@@ -130,4 +144,7 @@ class Setting < RailsSettings::Base
|
|||||||
|
|
||||||
field :rs_storage_url, type: :string,
|
field :rs_storage_url, type: :string,
|
||||||
default: ENV["RS_STORAGE_URL"].presence
|
default: ENV["RS_STORAGE_URL"].presence
|
||||||
|
|
||||||
|
field :rs_redis_url, type: :string,
|
||||||
|
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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/,
|
||||||
@@ -70,6 +72,7 @@ class User < ApplicationRecord
|
|||||||
# E-Mail update confirmed
|
# E-Mail update confirmed
|
||||||
LdapManager::UpdateEmail.call(self.dn, self.email)
|
LdapManager::UpdateEmail.call(self.dn, self.email)
|
||||||
else
|
else
|
||||||
|
# TODO Make configurable
|
||||||
# E-Mail from signup confirmed (i.e. account activation)
|
# E-Mail from signup confirmed (i.e. account activation)
|
||||||
enable_service %w[ discourse gitea mediawiki xmpp ]
|
enable_service %w[ discourse gitea mediawiki xmpp ]
|
||||||
|
|
||||||
@@ -107,6 +110,11 @@ class User < ApplicationRecord
|
|||||||
"#{self.cn}@#{self.ou}"
|
"#{self.cn}@#{self.ou}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mastodon_address
|
||||||
|
return nil unless Setting.mastodon_enabled?
|
||||||
|
"#{self.cn}@#{Setting.mastodon_address_domain}"
|
||||||
|
end
|
||||||
|
|
||||||
def valid_attribute?(attribute_name)
|
def valid_attribute?(attribute_name)
|
||||||
self.valid?
|
self.valid?
|
||||||
self.errors[attribute_name].blank?
|
self.errors[attribute_name].blank?
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
16
app/views/admin/settings/services/_droneci.html.erb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<h3>Drone CI</h3>
|
||||||
|
<ul role="list">
|
||||||
|
<%= render FormElements::FieldsetToggleComponent.new(
|
||||||
|
form: f,
|
||||||
|
attribute: :droneci_enabled,
|
||||||
|
enabled: Setting.droneci_enabled?,
|
||||||
|
title: "Enable Drone CI integration",
|
||||||
|
description: "Drone CI configuration present and features enabled"
|
||||||
|
) %>
|
||||||
|
<% if Setting.droneci_enabled? %>
|
||||||
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :droneci_public_url,
|
||||||
|
title: "Public URL"
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
@@ -8,16 +8,14 @@
|
|||||||
description: "ejabberd configuration present and features enabled"
|
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,13 @@
|
|||||||
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 %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :mastodon_address_domain,
|
||||||
|
title: "User address domain"
|
||||||
|
) %>
|
||||||
<% 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,13 @@
|
|||||||
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 Base URL"
|
||||||
class: "w-full", disabled: true %>
|
) %>
|
||||||
<% end %>
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
|
key: :rs_redis_url,
|
||||||
|
title: "Redis URL"
|
||||||
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -7,73 +7,85 @@
|
|||||||
services:
|
services:
|
||||||
</p>
|
</p>
|
||||||
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<% if Setting.ejabberd_enabled? %>
|
||||||
bg-cover bg-[center_top_-50px] bg-no-repeat
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-[url(/img/logos/icon_xmpp.svg)]">
|
bg-cover bg-[center_top_-50px] bg-no-repeat
|
||||||
<%= link_to "https://wiki.kosmos.org/Services:Chat",
|
bg-[url(/img/logos/icon_xmpp.svg)]">
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
<%= link_to services_chat_path,
|
||||||
<h3 class="mb-3.5">Chat</h3>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<p class="text-gray-600">
|
<h3 class="mb-3.5">Chat</h3>
|
||||||
Federated chat rooms and instant messaging
|
<p class="text-gray-600">
|
||||||
</p>
|
Federated chat rooms and instant messaging
|
||||||
<% end %>
|
</p>
|
||||||
</div>
|
<% end %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
</div>
|
||||||
bg-[length:95%] bg-center bg-no-repeat
|
<% end %>
|
||||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
<% if Setting.mastodon_enabled? %>
|
||||||
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
bg-[length:80%] bg-[right_top_-30px] bg-no-repeat
|
||||||
<h3 class="mb-3.5">Discourse</h3>
|
bg-[url(/img/logos/icon_mastodon.svg)]">
|
||||||
<p class="text-gray-600">
|
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
Kosmos community forums and user support/help site
|
<h3 class="mb-3.5">Mastodon</h3>
|
||||||
</p>
|
<p class="text-gray-600">
|
||||||
<% end %>
|
Your account on the Open Social Web
|
||||||
</div>
|
</p>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<% end %>
|
||||||
bg-cover bg-[center_top_-20px] bg-no-repeat
|
</div>
|
||||||
bg-[url(/img/logos/icon_mediawiki.svg)]">
|
<% end %>
|
||||||
<%= link_to "https://wiki.kosmos.org",
|
<% if Setting.discourse_enabled? %>
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<h3 class="mb-3.5">Wiki</h3>
|
bg-[length:95%] bg-center bg-no-repeat
|
||||||
<p class="text-gray-600">
|
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||||
Kosmos documentation and knowledge base
|
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||||
</p>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<% end %>
|
<h3 class="mb-3.5">Discourse</h3>
|
||||||
</div>
|
<p class="text-gray-600">
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
Kosmos community forums and user support/help site
|
||||||
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
|
</p>
|
||||||
bg-[url(/img/logos/icon_lightning.svg)]">
|
<% end %>
|
||||||
<%= link_to services_lightning_index_path,
|
</div>
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
<% end %>
|
||||||
<h3 class="mb-3.5">Lightning Network</h3>
|
<% if Setting.lndhub_enabled? %>
|
||||||
<p class="text-gray-600">
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
Send and receive sats over the Bitcoin Lightning Network
|
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
|
||||||
</p>
|
bg-[url(/img/logos/icon_lightning.svg)]">
|
||||||
<% end %>
|
<%= link_to services_lightning_index_path,
|
||||||
</div>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<h3 class="mb-3.5">Lightning Network</h3>
|
||||||
bg-cover bg-center bg-no-repeat
|
<p class="text-gray-600">
|
||||||
bg-[url(/img/logos/icon_gitea.png)]">
|
Send and receive sats over the Bitcoin Lightning Network
|
||||||
<%= link_to "https://gitea.kosmos.org",
|
</p>
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
<% end %>
|
||||||
<h3 class="mb-3.5">Gitea</h3>
|
</div>
|
||||||
<p class="text-gray-600">
|
<% end %>
|
||||||
Code hosting and collaboration for software projects
|
<% if Setting.gitea_enabled? %>
|
||||||
</p>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<% end %>
|
bg-cover bg-center bg-no-repeat
|
||||||
</div>
|
bg-[url(/img/logos/icon_gitea.png)]">
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<%= link_to Setting.gitea_public_url,
|
||||||
bg-cover bg-[center_top_-70px] bg-no-repeat
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
bg-[url(/img/logos/icon_droneci.svg)]">
|
<h3 class="mb-3.5">Gitea</h3>
|
||||||
<%= link_to "https://drone.kosmos.org",
|
<p class="text-gray-600">
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
Code hosting and collaboration for software projects
|
||||||
<h3 class="mb-3.5">Drone CI</h3>
|
</p>
|
||||||
<p class="text-gray-600">
|
<% end %>
|
||||||
Continuous integration for software projects on Gitea
|
</div>
|
||||||
</p>
|
<% end %>
|
||||||
<% end %>
|
<% if Setting.droneci_enabled? %>
|
||||||
</div>
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<% if Setting.remotestorage_enabled? && Flipper.enabled?(:remotestorage, current_user) %>
|
bg-cover bg-[center_top_-70px] bg-no-repeat
|
||||||
|
bg-[url(/img/logos/icon_droneci.svg)]">
|
||||||
|
<%= link_to Setting.droneci_public_url,
|
||||||
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
|
<h3 class="mb-3.5">Drone CI</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Continuous integration for software projects on Gitea
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if Setting.remotestorage_enabled? &&
|
||||||
|
Flipper.enabled?(:remotestorage, current_user) %>
|
||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
||||||
<%= link_to services_storage_path,
|
<%= link_to services_storage_path,
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
@@ -84,16 +96,19 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<!-- <div class="border border-gray-300 rounded-md hover:border-gray-400 -->
|
<% if Setting.mediawiki_enabled? %>
|
||||||
<!-- bg-[length:80%] bg-[right_top_-30px] bg-no-repeat -->
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
<!-- bg-[url(/img/logos/icon_mastodon.svg)]"> -->
|
bg-cover bg-[center_top_-20px] bg-no-repeat
|
||||||
<!-- <%= link_to "https://kosmos.social", class: "block h-full px-6 py-6 rounded-md" do %> -->
|
bg-[url(/img/logos/icon_mediawiki.svg)]">
|
||||||
<!-- <h3 class="mb-3.5">Mastodon</h3> -->
|
<%= link_to Setting.mediawiki_public_url,
|
||||||
<!-- <p class="text-gray-400"> -->
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<!-- Your account on the Open Social Web -->
|
<h3 class="mb-3.5">Wiki</h3>
|
||||||
<!-- </p> -->
|
<p class="text-gray-600">
|
||||||
<!-- <% end %> -->
|
Kosmos documentation and knowledge base
|
||||||
<!-- </div> -->
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<%= 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, value: h(params[:cn]),
|
||||||
|
autofocus: params[:cn].blank?, autocomplete: "username",
|
||||||
required: true, class: "relative grow", tabindex: "1" %>
|
required: true, class: "relative grow", tabindex: "1" %>
|
||||||
<span class="relative shrink-0 text-gray-500">@ <%= Setting.primary_domain %></span>
|
<span class="relative shrink-0 text-gray-500">@ <%= Setting.primary_domain %></span>
|
||||||
</p>
|
</p>
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
<p class="mb-8">
|
<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", tabindex: "2" %>
|
autofocus: params[:cn].present?, required: true,
|
||||||
|
class: "w-full", tabindex: "2" %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<%= tag.div class: "flex items-center mb-8 gap-x-3", data: {
|
<%= tag.div class: "flex items-center mb-8 gap-x-3", data: {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle <%= custom_class %>"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 445 B |
1
app/views/icons/_asterisk.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512" fill="currentColor" stroke="currentColor" stroke-width="2" class="<%= custom_class %>"><path d="M475.31 364.144L288 256l187.31-108.144c5.74-3.314 7.706-10.653 4.392-16.392l-4-6.928c-3.314-5.74-10.653-7.706-16.392-4.392L272 228.287V12c0-6.627-5.373-12-12-12h-8c-6.627 0-12 5.373-12 12v216.287L52.69 120.144c-5.74-3.314-13.079-1.347-16.392 4.392l-4 6.928c-3.314 5.74-1.347 13.079 4.392 16.392L224 256 36.69 364.144c-5.74 3.314-7.706 10.653-4.392 16.392l4 6.928c3.314 5.74 10.653 7.706 16.392 4.392L240 283.713V500c0 6.627 5.373 12 12 12h8c6.627 0 12-5.373 12-12V283.713l187.31 108.143c5.74 3.314 13.079 1.347 16.392-4.392l4-6.928c3.314-5.74 1.347-13.079-4.392-16.392z"/></svg>
|
||||||
|
After Width: | Height: | Size: 760 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder <%= custom_class %>"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 311 B After Width: | Height: | Size: 331 B |
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,27 @@
|
|||||||
</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"
|
||||||
<input type="text" disabled class="relative grow"
|
data-controller="clipboard modal"
|
||||||
|
data-action="keydown.esc->modal#close">
|
||||||
|
<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
@@ -0,0 +1,58 @@
|
|||||||
|
<%= render HeaderCompactComponent.new(title: "Storage") %>
|
||||||
|
|
||||||
|
<%= render MainCompactComponent.new do %>
|
||||||
|
<section class="permissions">
|
||||||
|
<p class="mb-8">
|
||||||
|
The app on
|
||||||
|
<%= link_to @client_id, "https://#{@client_id}", class: "ks-text-link" %>
|
||||||
|
is asking for access to these folders:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if @root_access_requested %>
|
||||||
|
<p class="scope text-lg">
|
||||||
|
<span class="text-red-700">
|
||||||
|
<%= render partial: "icons/alert-triangle",
|
||||||
|
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
|
||||||
|
All files and directories
|
||||||
|
</span>
|
||||||
|
<% if (@scopes & [":r"]).any? %>
|
||||||
|
<span class="text-sm text-gray-500">(read only)</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<% @scopes.each do |scope| %>
|
||||||
|
<p class="scope text-gray-600">
|
||||||
|
<span class="text-lg">
|
||||||
|
<%= render partial: "icons/folder",
|
||||||
|
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
|
||||||
|
<%= scope_name(scope) %>
|
||||||
|
</span>
|
||||||
|
<% if scope_permissions(scope) == "r" %>
|
||||||
|
<span>(read only)</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form_with(url: rs_oauth_path, method: :post, data: { turbo: false }) do |f| %>
|
||||||
|
<%= f.hidden_field :redirect_uri, value: @redirect_uri %>
|
||||||
|
<%= f.hidden_field :scope, value: @scopes.join(" ") %>
|
||||||
|
<%= f.hidden_field :user_id, value: @user.id %>
|
||||||
|
<%= f.hidden_field :client_id, value: @client_id %>
|
||||||
|
<%= f.hidden_field :state, value: @state %>
|
||||||
|
<p class="mt-8 mb-6">
|
||||||
|
<%= f.label :expire_at, "Permission expires:", class: "mr-1.5" %>
|
||||||
|
<%= f.select :expire_at, options_for_select(@expire_at_dates) %>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
You can revoke access for this app at any time on your storage dashboard.
|
||||||
|
</p>
|
||||||
|
<p class="mt-8 flex flex-col sm:flex-row gap-3 sm:gap-2 sm:justify-items-stretch">
|
||||||
|
<%= f.submit "Allow",
|
||||||
|
class: "btn-md btn-blue w-full sm:order-last sm:grow",
|
||||||
|
data: { disable_with: "Saving..." } %>
|
||||||
|
<%= link_to "Deny", @denial_url, class: "btn-md btn-gray text-red-700 w-full sm:grow" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
199
app/views/services/chat/show.html.erb
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Chat") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<p class="mb-6">
|
||||||
|
Chat with anyone on the open Jabber (XMPP) network. Message people directly, or
|
||||||
|
join public channels or private rooms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section data-controller="modal" data-action="keydown.esc->modal#close">
|
||||||
|
<h3>Your Chat Address</h3>
|
||||||
|
<p class="mb-6">
|
||||||
|
When you exchange contacts with people, give them your
|
||||||
|
address, or add them using their address:
|
||||||
|
</p>
|
||||||
|
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
|
||||||
|
<input type="text" id="user_address" class="grow"
|
||||||
|
value=<%= current_user.address %> disabled="disabled"
|
||||||
|
data-clipboard-target="source" />
|
||||||
|
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||||
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<span class="content-initial">
|
||||||
|
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
<span class="content-active hidden">
|
||||||
|
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||||
|
data-action="click->modal#open" title="Show QR code">
|
||||||
|
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<%= render QrCodeModalComponent.new(qr_content: "xmpp:"+current_user.address) %>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Chat Apps</h3>
|
||||||
|
<p>
|
||||||
|
Use your account with many different apps, and on any devices you wish!
|
||||||
|
When opening an app for the first time, just enter your user address and
|
||||||
|
password to log in.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Recommended Apps</h3>
|
||||||
|
<div data-controller="tabs"
|
||||||
|
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
|
||||||
|
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
|
||||||
|
class="mb-12">
|
||||||
|
<select data-action="tabs#change" data-tabs-target="select"
|
||||||
|
class="block w-full mb-8 sm:hidden">
|
||||||
|
<optgroup label="Mobile">
|
||||||
|
<option>Android</option>
|
||||||
|
<option>iOS</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Desktop">
|
||||||
|
<option>Linux</option>
|
||||||
|
<option>Windows</option>
|
||||||
|
<option>macOS</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
Android
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
iOS
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
Linux
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
Windows
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
macOS
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change"> -->
|
||||||
|
<!-- <a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline"> -->
|
||||||
|
<!-- Web -->
|
||||||
|
<!-- </a> -->
|
||||||
|
<!-- </li> -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div id="apps-android" class="hidden grid grid-cols-1 gap-6"
|
||||||
|
data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Conversations",
|
||||||
|
description: "The gold standard for Jabber on mobile devices",
|
||||||
|
icon_path: "/img/logos/icon_conversations.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://conversations.im"],
|
||||||
|
["Google Play", "https://play.google.com/store/apps/details?id=eu.siacs.conversations"],
|
||||||
|
["F-Droid", "https://f-droid.org/en/packages/eu.siacs.conversations/"],
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div id="apps-ios" class="hidden grid grid-cols-1 gap-6"
|
||||||
|
data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Siskin IM",
|
||||||
|
description: "Lightweight and powerful chat app for iPhone and iPad",
|
||||||
|
icon_path: "/img/logos/logo_siskin.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://siskin.im"],
|
||||||
|
["App Store", "https://apps.apple.com/us/app/tigase-messenger/id1153516838"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Monal",
|
||||||
|
description: "A chat app for iOS, iPadOS, and macOS",
|
||||||
|
icon_path: "/img/logos/icon_monal.svg",
|
||||||
|
icon_fill_box: true,
|
||||||
|
links: [
|
||||||
|
["Website", "https://monal-im.org"],
|
||||||
|
["App Store", "https://apps.apple.com/app/id317711500"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div id="apps-linux" class="hidden grid grid-cols-1 gap-6"
|
||||||
|
data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Dino",
|
||||||
|
description: "A modern and simple chat app for Linux (good for GNOME)",
|
||||||
|
icon_path: "/img/logos/icon_dino.svg",
|
||||||
|
links: [
|
||||||
|
["Website", "https://dino.im"],
|
||||||
|
["Install from package", "https://github.com/dino/dino/wiki/Distribution-Packages"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Kaidan",
|
||||||
|
description: "A fairly new, user-friendly chat app for all devices (good for KDE)",
|
||||||
|
icon_path: "/img/logos/icon_kaidan.svg",
|
||||||
|
links: [
|
||||||
|
["Website", "https://kaidan.im"],
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Gajim",
|
||||||
|
description: "A fully-featured chat app for Linux and Windows",
|
||||||
|
icon_path: "/img/logos/icon_gajim.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://gajim.org/"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
|
||||||
|
data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Gajim",
|
||||||
|
description: "A fully-featured chat app for Linux and Windows",
|
||||||
|
icon_path: "/img/logos/icon_gajim.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://gajim.org/"],
|
||||||
|
["Microsoft Store", "https://apps.microsoft.com/store/detail/9PGGF6HD43F9?launch=true&mode=mini"],
|
||||||
|
["Download options", "https://gajim.org/download/"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div id="apps-mac" class="hidden grid grid-cols-1 gap-6"
|
||||||
|
data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Beagle IM",
|
||||||
|
description: "Lightweight and powerful chat app for macOS",
|
||||||
|
icon_path: "/img/logos/logo_beagle.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://beagle.im"],
|
||||||
|
["App Store", "https://apps.apple.com/us/app/beagleim-by-tigase-inc/id1445349494"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Monal",
|
||||||
|
description: "A chat app for iOS, iPadOS, and macOS",
|
||||||
|
icon_path: "/img/logos/icon_monal.svg",
|
||||||
|
icon_fill_box: true,
|
||||||
|
links: [
|
||||||
|
["Website", "https://monal-im.org"],
|
||||||
|
["App Store", "https://apps.apple.com/app/id1637078500"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="hidden grid grid-cols-1 gap-4 sm:gap-6" data-tabs-target="panel"> -->
|
||||||
|
<!-- Web -->
|
||||||
|
<!-- </div> -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
@@ -16,20 +16,20 @@
|
|||||||
<input type="text" id="user_address" class="grow"
|
<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" data-action="keydown.esc->modal#close">
|
||||||
<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>
|
|
||||||
|
|||||||
219
app/views/services/mastodon/show.html.erb
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Social") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<p class="mb-6">
|
||||||
|
Follow and interact with anyone on the open social web, from your Kosmos Mastodon account.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section data-controller="modal" data-action="keydown.esc->modal#close">
|
||||||
|
<h3>Your User Address</h3>
|
||||||
|
<p class="mb-6">
|
||||||
|
Others can follow you under this address:
|
||||||
|
</p>
|
||||||
|
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
|
||||||
|
<input type="text" id="user_address" class="grow"
|
||||||
|
value=<%= current_user.mastodon_address %> disabled="disabled"
|
||||||
|
data-clipboard-target="source" />
|
||||||
|
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
|
||||||
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<span class="content-initial">
|
||||||
|
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
<span class="content-active hidden">
|
||||||
|
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
|
||||||
|
data-action="click->modal#open" title="Show QR code">
|
||||||
|
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<%= render QrCodeModalComponent.new(qr_content: current_user.address) %>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Social Apps</h3>
|
||||||
|
<p>
|
||||||
|
Use your Mastodon account with many different apps, and on any devices
|
||||||
|
you wish! When adding your account to an app, you will log in via
|
||||||
|
<a href="https://kosmos.social" target="_blank" class="ks-text-link">kosmos.social</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Recommended Apps</h3>
|
||||||
|
<div data-controller="tabs"
|
||||||
|
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
|
||||||
|
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
|
||||||
|
class="mb-12">
|
||||||
|
<select data-action="tabs#change" data-tabs-target="select"
|
||||||
|
class="block w-full mb-8 sm:hidden">
|
||||||
|
<option>Web</option>
|
||||||
|
<optgroup label="Mobile">
|
||||||
|
<option>Android</option>
|
||||||
|
<option>iOS</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Desktop">
|
||||||
|
<option>Linux</option>
|
||||||
|
<option>Windows</option>
|
||||||
|
<option>macOS</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
Web
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-5 font-semibold no-underline">
|
||||||
|
Android
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
iOS
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
Linux
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
Windows
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
|
||||||
|
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
|
||||||
|
macOS
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "kosmos.social",
|
||||||
|
description: "The official Web app",
|
||||||
|
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||||
|
links: [
|
||||||
|
["Launch", "https://kosmos.social"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Elk",
|
||||||
|
description: " A nimble Mastodon web client",
|
||||||
|
icon_path: "/img/logos/icon_elk.svg",
|
||||||
|
links: [
|
||||||
|
["Launch", "https://elk.zone"],
|
||||||
|
["GitHub", "https://github.com/elk-zone/elk"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Sengi",
|
||||||
|
description: "A cross-platform app, inspired by TweetDeck",
|
||||||
|
icon_path: "/img/logos/icon_sengi.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://nicolasconstant.github.io/sengi/"],
|
||||||
|
["GitHub", "https://github.com/NicolasConstant/sengi"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Mastodon for Android",
|
||||||
|
description: "Android client by the Mastodon core team",
|
||||||
|
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||||
|
links: [
|
||||||
|
["Website", "https://joinmastodon.org/apps"],
|
||||||
|
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Fedilab",
|
||||||
|
description: "Android client with many features",
|
||||||
|
icon_path: "/img/logos/icon_fedilab.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://fedilab.app"],
|
||||||
|
["Google Play", "https://play.google.com/store/apps/details?id=app.fedilab.android"],
|
||||||
|
["F-Droid", "https://f-droid.org/packages/fr.gouv.etalab.mastodon"],
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Megalodon",
|
||||||
|
description: "A popular fork of the official Android app",
|
||||||
|
icon_path: "/img/logos/icon_megalodon.png",
|
||||||
|
icon_fill_box: true,
|
||||||
|
links: [
|
||||||
|
["Website", "https://sk22.github.io/megalodon/"],
|
||||||
|
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Mastodon for iOS",
|
||||||
|
description: "iOS client by the Mastodon core team",
|
||||||
|
icon_path: "/img/logos/icon_mastodon-2.svg",
|
||||||
|
links: [
|
||||||
|
["Website", "https://joinmastodon.org/apps"],
|
||||||
|
["App Store", "https://apps.apple.com/us/app/mastodon-for-iphone/id1571998974"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Ice Cubes",
|
||||||
|
description: "Slick, fast, open source, and with customizable UI",
|
||||||
|
icon_path: "/img/logos/icon_icecubes.png",
|
||||||
|
icon_fill_box: true,
|
||||||
|
links: [
|
||||||
|
["App Store", "https://apps.apple.com/us/app/ice-cubes-for-mastodon/id6444915884"],
|
||||||
|
["GitHub", "https://github.com/Dimillian/IceCubesApp"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Mammoth",
|
||||||
|
description: " Powerful, fast, feature-rich",
|
||||||
|
icon_path: "/img/logos/icon_mammoth.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://getmammoth.app/"],
|
||||||
|
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Tuba",
|
||||||
|
description: "A simple, fast Mastodon app for Linux (good on GNOME)",
|
||||||
|
icon_path: "/img/logos/icon_tuba.svg",
|
||||||
|
links: [
|
||||||
|
["Website", "https://tuba.geopjr.dev"],
|
||||||
|
["Flathub", "https://flathub.org/apps/dev.geopjr.Tuba"],
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Sengi",
|
||||||
|
description: "A cross-platform app, inspired by TweetDeck",
|
||||||
|
icon_path: "/img/logos/icon_sengi.png",
|
||||||
|
links: [
|
||||||
|
["Website", "https://nicolasconstant.github.io/sengi/"],
|
||||||
|
["GitHub", "https://github.com/NicolasConstant/sengi"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
|
||||||
|
<%= render AppInfoComponent.new(
|
||||||
|
name: "Mastonaut",
|
||||||
|
description: "Simple, elegant, and native Mastodon client for Mac",
|
||||||
|
icon_path: "/img/logos/icon_mastonaut.png",
|
||||||
|
links: [
|
||||||
|
["Launch", "https://www.mastonaut.app"],
|
||||||
|
["Mac App Store", "https://apps.apple.com/app/mastonaut/id1450757574"]
|
||||||
|
]
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
icon: Setting.discourse_enabled? ? "check" : "x",
|
icon: Setting.discourse_enabled? ? "check" : "x",
|
||||||
active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
|
active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
|
||||||
) %>
|
) %>
|
||||||
|
<%= render SidenavLinkComponent.new(
|
||||||
|
level: 2,
|
||||||
|
name: "Drone CI",
|
||||||
|
path: admin_settings_services_path(params: { s: "droneci" }),
|
||||||
|
icon: Setting.droneci_enabled? ? "check" : "x",
|
||||||
|
active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
|
||||||
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
level: 2,
|
level: 2,
|
||||||
name: "ejabberd",
|
name: "ejabberd",
|
||||||
|
|||||||
6
app/views/shared/status_bad_request.html.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<%= render HeaderCompactComponent.new(title: "404") %>
|
||||||
|
|
||||||
|
<%= render MainCompactComponent.new do %>
|
||||||
|
<h2>Bad request</h2>
|
||||||
|
<p>Please go back and try again.</p>
|
||||||
|
<% end %>
|
||||||
@@ -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" # @4.0.3
|
||||||
|
|||||||
@@ -21,12 +21,16 @@ Rails.application.routes.draw do
|
|||||||
namespace :services do
|
namespace :services do
|
||||||
get 'storage', to: 'remotestorage#dashboard'
|
get 'storage', to: 'remotestorage#dashboard'
|
||||||
|
|
||||||
|
resource :chat, only: [:show], controller: 'chat'
|
||||||
|
|
||||||
resources :lightning, only: [:index] do
|
resources :lightning, only: [:index] do
|
||||||
collection do
|
collection do
|
||||||
get 'transactions'
|
get 'transactions'
|
||||||
get 'qr_lnurlp'
|
get 'qr_lnurlp'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resource :mastodon, only: [:show], controller: 'mastodon'
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :settings, param: 'section', only: ['index', 'show', 'update'] do
|
resources :settings, param: 'section', only: ['index', 'show', 'update'] do
|
||||||
@@ -66,7 +70,14 @@ 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', create: ':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,62 @@ 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
|
RS_REDIS_URL: redis://redis:6379/1
|
||||||
# LDAP_USE_TLS: "false"
|
LDAP_HOST: ldap
|
||||||
# depends_on:
|
LDAP_PORT: 3389
|
||||||
# - ldap
|
LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
# - redis
|
LDAP_USE_TLS: "false"
|
||||||
|
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
|
RS_REDIS_URL: redis://redis:6379/1
|
||||||
# LDAP_ADMIN_PASSWORD: passthebutter
|
LDAP_HOST: ldap
|
||||||
# LDAP_USE_TLS: "false"
|
LDAP_PORT: 3389
|
||||||
# LAUNCHY_DRY_RUN: true
|
LDAP_ADMIN_PASSWORD: passthebutter
|
||||||
# BROWSER: /dev/null
|
LDAP_USE_TLS: "false"
|
||||||
# depends_on:
|
LAUNCHY_DRY_RUN: true
|
||||||
# - ldap
|
BROWSER: /dev/null
|
||||||
# - redis
|
depends_on:
|
||||||
|
- ldap
|
||||||
|
- redis
|
||||||
|
|
||||||
# phpldapadmin:
|
# phpldapadmin:
|
||||||
# image: osixia/phpldapadmin:0.9.0
|
# image: osixia/phpldapadmin:0.9.0
|
||||||
|
|||||||
@@ -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.7.0",
|
"version": "0.8.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"
|
||||||
|
|||||||
BIN
public/img/logos/icon_conversations.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
1
public/img/logos/icon_dino.svg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
42
public/img/logos/icon_elk.svg
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="250"
|
||||||
|
height="250"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg30"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs34" />
|
||||||
|
<mask
|
||||||
|
id="a"
|
||||||
|
width="240"
|
||||||
|
height="234"
|
||||||
|
x="4"
|
||||||
|
y="1"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
style="mask-type:alpha">
|
||||||
|
<path
|
||||||
|
fill="white"
|
||||||
|
d="M244 123c0 64.617-38.383 112-103 112-64.617 0-103-30.883-103-95.5C38 111.194-8.729 36.236 8 16 29.46-9.959 88.689 6 125 6c64.617 0 119 52.383 119 117Z"
|
||||||
|
id="path19" />
|
||||||
|
</mask>
|
||||||
|
<g
|
||||||
|
mask="url(#a)"
|
||||||
|
id="g28"
|
||||||
|
transform="matrix(0.90923731,0,0,1.0049564,13.520015,-3.1040835)">
|
||||||
|
<path
|
||||||
|
fill="#ea9e44"
|
||||||
|
d="m 116.94,88.1 c -13.344,1.552 -20.436,-2.019 -24.706,10.71 0,0 14.336,21.655 52.54,21.112 -2.135,8.848 -1.144,15.368 -1.144,23.207 0,26.079 -20.589,48.821 -65.961,48.821 -23.03,0 -51.015,4.191 -72.367,15.911 -15.175,8.305 -27.048,20.336 -32.302,37.023 l 5.956,8.461 11.4,0.155 v 47.889 l -13.91,21.966 3.998,63.645 H -6.364 L -5.22,335.773 C 1.338,331.892 16.36,321.802 29.171,306.279 46.557,285.4 59.902,255.052 44.193,217.486 l 11.744,-5.045 c 12.887,30.814 8.388,57.514 -2.898,79.013 21.58,-0.698 40.11,-2.095 55.819,-4.734 l -3.584,-43.698 12.659,-1.087 L 129.98,387 h 13.116 l 2.212,-94.459 c 10.447,-4.502 34.239,-21.034 45.372,-78.47 1.372,-6.986 2.135,-12.885 2.516,-17.93 1.754,-12.806 2.745,-27.243 3.051,-43.698 l -18.683,-5.976 h 57.42 l 5.567,-12.807 c -5.414,0.233 -11.896,-2.639 -11.896,-2.639 l 1.297,-6.209 H 242 L 176.801,90.428 c -7.244,2.794 -14.87,6.442 -20.208,10.866 -4.27,-3.105 -19.063,-12.807 -39.653,-13.195 z"
|
||||||
|
id="path22" />
|
||||||
|
<path
|
||||||
|
fill="#c16929"
|
||||||
|
d="M 6.217,24.493 18.494,21 c 5.948,21.577 13.345,33.375 22.648,39.352 8.388,5.099 19.75,5.239 31.799,4.579 C 69.433,63.767 66.154,62.137 63.104,59.886 56.317,54.841 50.522,46.458 46.175,31.246 l 12.201,-3.649 c 3.279,11.488 7.092,18.085 12.201,21.888 5.11,3.726 11.286,4.657 18.606,5.433 13.726,1.553 30.884,2.174 52.312,12.264 2.898,1.086 5.872,2.483 8.769,4.036 -0.381,-0.776 -0.762,-1.553 -1.296,-2.406 -3.66,-5.822 -10.828,-11.953 -24.097,-16.92 l 4.27,-12.109 c 21.581,7.917 30.121,19.171 33.553,28.097 3.965,10.168 1.525,18.124 1.525,18.124 -3.05,1.009 -6.1,2.406 -9.608,3.492 -6.634,-4.579 -12.887,-8.033 -18.835,-10.75 C 113.814,70.442 92.31,76.108 73.246,77.893 58.91,79.213 45.794,78.591 34.432,71.295 23.222,64.155 13.385,50.495 6.217,24.493 Z"
|
||||||
|
id="path24" />
|
||||||
|
<path
|
||||||
|
fill="#c16929"
|
||||||
|
d="M 90.098,45.294 C 87.582,39.55 86.057,32.487 86.743,23.794 l 12.659,0.932 c -0.763,10.555 2.897,17.696 7.015,22.353 -5.338,-0.931 -10.447,-1.04 -16.319,-1.785 z m 80.069,-1.32 8.312,-9.702 c 21.58,19.094 8.159,46.415 8.159,46.415 l -11.819,-1.32 c -0.382,-6.24 -1.144,-17.836 -6.635,-24.371 3.584,1.84 6.635,3.865 9.99,6.908 0,-5.666 -1.754,-12.341 -8.007,-17.93 z"
|
||||||
|
id="path26" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/logos/icon_fedilab.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/img/logos/icon_gajim.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/logos/icon_icecubes.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
1
public/img/logos/icon_kaidan.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="256.39" width="256.39"><defs><radialGradient xlink:href="#b" id="g" cx="97.347" cy="925.562" fx="97.347" fy="925.562" r="59.347" gradientTransform="matrix(0 .94357 -2.19206 0 2158.799 18.862)" gradientUnits="userSpaceOnUse"/><radialGradient xlink:href="#b" id="h" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 .8425 -1.93775 0 1946.71 -14.002)" cx="95.806" cy="937.536" fx="95.806" fy="937.536" r="59.347"/><radialGradient id="a" gradientUnits="userSpaceOnUse" cy="256" cx="256" gradientTransform="matrix(2.2585 .02063 -.02134 2.336 -316.73 -347.29)" r="249.14"><stop stop-color="#30c357" offset="0"/><stop offset=".5" stop-color="#2cb465"/><stop stop-color="#32a482" offset="1"/></radialGradient><radialGradient xlink:href="#b" id="f" cx="88.754" cy="123.035" fx="88.754" fy="123.035" r="78.3" gradientTransform="matrix(.02885 1.09003 -1.35878 .03597 300.127 33.098)" gradientUnits="userSpaceOnUse"/><linearGradient id="b"><stop offset="0" stop-opacity=".416"/><stop offset="1" stop-opacity="0"/></linearGradient><linearGradient xlink:href="#a" id="e" gradientUnits="userSpaceOnUse" x1="127.988" y1="808.969" x2="127.988" y2="1033.499" gradientTransform="translate(18.596 -806.496) scale(1.011)"/><linearGradient xlink:href="#a" id="d" gradientUnits="userSpaceOnUse" x1="127.988" y1="808.969" x2="127.988" y2="1033.499" gradientTransform="translate(18.596 -806.496) scale(1.011)"/><filter id="c" x="-.041" width="1.082" y="-.04" height="1.081" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="3.891"/></filter></defs><g transform="translate(82 -2.286)" opacity=".512" fill-opacity=".509" filter="url(#c)"><path d="M47.992 16.371c-62.684 0-113.5 50.816-113.5 113.5 0 15.92 3.284 31.071 9.201 44.822 2.875 7.87 5.724 15.722 6.72 22.264 1.01 6.646.108 11.944-1.16 15.361-1.266 3.418-2.901 4.955-4.165 8.502-1.264 3.548-2.155 9.106.775 13.319 2.93 4.213 9.68 7.08 22.17 9.277 12.49 2.197 30.725 3.719 46.623 3.719 15.899 0 29.471-1.536 39.037-2.932 8.418-1.228 13.704-2.35 18.643-3.459.557-.122 1.114-.241 1.668-.37l.363-.083-.01-.006c49.969-11.888 87.135-56.807 87.135-110.414 0-62.684-50.816-113.5-113.5-113.5z" fill-opacity="1" paint-order="stroke fill markers"/></g><g fill="#1f6d52"><path d="M241.492 125.586c0 62.684-50.816 113.5-113.5 113.5s-113.5-50.816-113.5-113.5 50.816-113.5 113.5-113.5 113.5 50.816 113.5 113.5z" paint-order="stroke fill markers"/><path d="M154.368 236.005c-5.556 1.258-11.108 2.516-20.674 3.912-9.566 1.397-23.139 2.93-39.037 2.933-15.899 0-34.133-1.524-46.623-3.72-12.49-2.197-19.241-5.064-22.171-9.277-2.93-4.213-2.038-9.77-.774-13.318 1.263-3.548 2.898-5.086 4.165-8.503 1.268-3.418 2.168-8.716 1.158-15.361-1.011-6.646-3.932-14.64-6.853-22.634"/></g><path d="M261.492 124.872c0 62.684-50.816 113.5-113.5 113.5s-113.5-50.816-113.5-113.5c0-62.685 50.816-113.5 113.5-113.5s113.5 50.815 113.5 113.5z" fill="url(#d)" paint-order="stroke fill markers" transform="translate(-20 -4.286)"/><path d="M174.368 235.29c-5.556 1.26-11.108 2.517-20.674 3.913-9.566 1.396-23.139 2.93-39.037 2.933-15.899 0-34.133-1.524-46.623-3.72-12.49-2.197-19.241-5.064-22.101-8.228-2.86-3.163-1.828-6.622-.565-10.17 1.264-3.547 2.759-7.184 3.956-11.651 1.198-4.467 2.098-9.765 1.088-16.41-1.011-6.646-3.932-14.64-6.853-22.635" fill="url(#e)" transform="translate(-20 -4.286)"/><path d="M188.207 160.324l-82.176 19.83-19.979-20.038S72.97 173.377 67.98 179.894l46.02 46.82 14.242 7.371c10.099-1.39 19.726-3.117 28.797-5.267.013 0 .026-.01.04-.01 27.199-7.275 48.656-19.338 63.52-36.12z" fill="url(#f)"/><path d="M205.473 113.95L49.639 132.596l30 30 147.375 21.975c7.02-9.995 12.02-21.406 14.867-34.211z" fill="url(#g)"/><path d="M188.46 68.752l45 45-142.557-2.826-24-24z" fill="url(#h)"/><g transform="translate(-88.78 -687.44) scale(1.011)" fill="#c1ede5"><rect x="150.87" y="744.15" width="127.26" height="26.476" ry="13.238" rx="13.238"/><rect x="133.62" y="789.12" width="161.61" height="26.476" ry="13.238" rx="13.238"/><rect x="186.23" y="834.92" width="91.833" height="26.476" ry="13.238" rx="13.238"/><rect x="150.8" y="834.92" width="26.476" height="26.476" ry="13.238" rx="13.238"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/img/logos/icon_mammoth.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
65
public/img/logos/icon_mastodon-2.svg
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg13"
|
||||||
|
sodipodi:docname="icon_mastodon-2.svg"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview15"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="10.329114"
|
||||||
|
inkscape:cx="25.849265"
|
||||||
|
inkscape:cy="39.548407"
|
||||||
|
inkscape:window-width="1663"
|
||||||
|
inkscape:window-height="749"
|
||||||
|
inkscape:window-x="50"
|
||||||
|
inkscape:window-y="118"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg13" />
|
||||||
|
<a
|
||||||
|
id="a300"
|
||||||
|
transform="translate(12.930783,10.499984)">
|
||||||
|
<path
|
||||||
|
d="M 73.8393,17.4898 C 72.6973,9.00165 65.2994,2.31235 56.5296,1.01614 55.05,0.797115 49.4441,0 36.4582,0 h -0.097 C 23.3717,0 20.585,0.797115 19.1054,1.01614 10.5798,2.27644 2.79399,8.28712 0.904997,16.8758 -0.00358524,21.1056 -0.100549,25.7949 0.0682394,30.0965 0.308852,36.2651 0.355538,42.423 0.91577,48.5665 c 0.3873,4.0809 1.06295,8.1292 2.02186,12.1147 1.79562,7.3608 9.06427,13.4864 16.18567,15.9854 7.6245,2.6062 15.8241,3.0389 23.6806,1.2496 0.8643,-0.2011 1.7178,-0.4345 2.5606,-0.7002 1.9105,-0.6068 4.1478,-1.2855 5.7926,-2.4775 0.0226,-0.0168 0.0411,-0.0384 0.0541,-0.0632 0.0131,-0.0249 0.0204,-0.0524 0.0213,-0.0805 v -5.9532 c -4e-4,-0.0262 -0.0066,-0.052 -0.0183,-0.0755 -0.0117,-0.0235 -0.0284,-0.0441 -0.0491,-0.0603 -0.0207,-0.0162 -0.0447,-0.0275 -0.0703,-0.0332 -0.0256,-0.0057 -0.0522,-0.0056 -0.0777,3e-4 -5.0336,1.2021 -10.1917,1.8048 -15.3669,1.7953 -8.9063,0 -11.3016,-4.2262 -11.9876,-5.9856 -0.5513,-1.5206 -0.9014,-3.1067 -1.0414,-4.718 -0.0015,-0.0271 0.0035,-0.0541 0.0145,-0.0789 0.0109,-0.0248 0.0276,-0.0466 0.0486,-0.0637 0.021,-0.0172 0.0457,-0.0291 0.0722,-0.0349 0.0265,-0.0058 0.0539,-0.0053 0.0802,0.0015 4.9497,1.194 10.0237,1.7967 15.1155,1.7953 1.2246,0 2.4456,0 3.6702,-0.0323 5.1211,-0.1436 10.5187,-0.4057 15.5572,-1.3895 0.1257,-0.0252 0.2514,-0.0467 0.3591,-0.079 7.9474,-1.5261 15.5106,-6.3159 16.2791,-18.445 0.0287,-0.4775 0.1006,-5.0017 0.1006,-5.4972 0.0035,-1.684 0.5422,-11.946 -0.0791,-18.2511 z"
|
||||||
|
fill="url(#paint0_linear_549_34)"
|
||||||
|
id="path2"
|
||||||
|
style="fill:url(#paint0_linear_549_34)" />
|
||||||
|
<path
|
||||||
|
d="M 61.2484,27.0263 V 48.114 H 52.8916 V 27.6475 c 0,-4.3087 -1.7956,-6.5062 -5.4479,-6.5062 -4.015,0 -6.026,2.5996 -6.026,7.7342 V 40.0782 H 33.1111 V 28.8755 c 0,-5.1346 -2.0146,-7.7342 -6.0296,-7.7342 -3.6308,0 -5.4444,2.1975 -5.4444,6.5062 V 48.114 H 13.2839 V 27.0263 c 0,-4.3087 1.1001,-7.7317 3.3004,-10.2691 2.2696,-2.5314 5.2468,-3.8312 8.9421,-3.8312 4.2772,0 7.5093,1.6445 9.6641,4.9299 l 2.0793,3.4901 2.0829,-3.4901 c 2.1547,-3.2854 5.3868,-4.9299 9.6568,-4.9299 3.6918,0 6.6689,1.2998 8.9458,3.8312 2.1978,2.535 3.2955,5.958 3.2931,10.2691 z"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="path4" />
|
||||||
|
</a>
|
||||||
|
<defs
|
||||||
|
id="defs11">
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_549_34"
|
||||||
|
x1="37.069199"
|
||||||
|
y1="0"
|
||||||
|
x2="37.069199"
|
||||||
|
y2="79"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#6364FF"
|
||||||
|
id="stop6" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#563ACC"
|
||||||
|
id="stop8" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/img/logos/icon_mastonaut.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/img/logos/icon_megalodon.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
149
public/img/logos/icon_monal.svg
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="20mm"
|
||||||
|
height="20.085215mm"
|
||||||
|
viewBox="0 0 20 20.085215"
|
||||||
|
version="1.1"
|
||||||
|
id="svg3834"
|
||||||
|
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||||
|
sodipodi:docname="monal.svg"
|
||||||
|
style="enable-background:new"
|
||||||
|
inkscape:export-filename="/Users/anurodhp/Desktop/monal.png"
|
||||||
|
inkscape:export-xdpi="1300.48"
|
||||||
|
inkscape:export-ydpi="1300.48">
|
||||||
|
<defs
|
||||||
|
id="defs3828">
|
||||||
|
<inkscape:perspective
|
||||||
|
sodipodi:type="inkscape:persp3d"
|
||||||
|
inkscape:vp_x="0 : 10.042608 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_z="20 : 10.042608 : 1"
|
||||||
|
inkscape:persp3d-origin="10 : 6.6950717 : 1"
|
||||||
|
id="perspective841" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="9.0725624"
|
||||||
|
inkscape:cx="28.787632"
|
||||||
|
inkscape:cy="44.279616"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer3"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
inkscape:window-height="700"
|
||||||
|
inkscape:window-x="-4"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:pagecheckerboard="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3831">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-181.15717,-221.0978)"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer3"
|
||||||
|
inkscape:label="Layer 2"
|
||||||
|
style="display:inline">
|
||||||
|
<rect
|
||||||
|
id="rect30"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
x="181.15717"
|
||||||
|
y="221.18301"
|
||||||
|
style="stroke-width:0.26458332" />
|
||||||
|
<rect
|
||||||
|
style="fill:#2cd3e3;fill-opacity:1;stroke-width:0.26458332;opacity:1"
|
||||||
|
id="rect48"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
x="181.15717"
|
||||||
|
y="221.18301"
|
||||||
|
inkscape:export-xdpi="1299.6801"
|
||||||
|
inkscape:export-ydpi="1299.6801" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g25"
|
||||||
|
transform="matrix(1.25,0,0,1.25,-45.477532,-55.597438)">
|
||||||
|
<g
|
||||||
|
id="g4578"
|
||||||
|
transform="translate(0.8477441,0.34537723)">
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke-width:0.21166666"
|
||||||
|
d="m 191.56803,221.43167 c 0.44531,-0.54927 1.00283,-1.55943 1.42657,-1.47418 -1.6117,3.28945 -5.09363,5.55983 -6.77643,8.41276 -1.48053,1.02259 -5.59221,4.97552 -3.50788,4.62647 2.55426,-0.33289 4.26942,-0.18434 6.47037,-0.0863 2.97857,0.44913 2.99008,0.35922 1.42237,-0.94939 -4.08407,-2.92741 -7.3886,-5.96422 -2.55345,-2.51434 1.19608,0.89899 3.58977,2.88011 4.62961,3.96256 0.14192,0.38081 -1.36875,-1.03083 -1.63269,-1.38151 1.00787,0.47021 4.41857,3.94275 5.65285,3.45204 1.11708,-0.36357 1.5434,-1.4884 2.64165,-0.91497 0.73976,-0.52896 -0.81203,-1.84714 0.109,-2.45498 1.18118,-0.18435 -0.2902,-1.51785 0.93686,-1.87197 0.72674,-0.62762 -1.09372,-1.01894 -1.52077,-1.45287 -0.89602,-0.45402 -0.35512,-1.55824 -0.004,-1.79086 0.0846,-1.03756 1.78245,-0.86434 2.36976,-1.66716 0.74024,-0.20999 1.24713,-1.13045 0.10892,-1.05465 -1.29104,-0.33585 -2.48308,0.3858 -3.65095,0.80824 -0.75744,0.43619 -1.51475,-0.3745 -0.67602,-0.9122 0.086,-0.87983 -0.49258,-4.71845 2.78132,-3.06301 0.34122,0.76448 -1.29924,0.73222 -1.56848,1.44997 -0.51725,0.58889 -0.82268,1.36738 -0.81609,2.1534 1.37222,-0.61392 1.59347,-2.39848 2.89681,-3.09724 0.62326,-0.9785 1.02124,0.86222 0.0967,0.71688 -0.41512,0.46396 -1.82667,1.32778 -1.43703,1.64655 0.99965,-0.74709 2.16479,-0.20775 3.17351,0.0358 1.13327,0.97208 -0.7695,1.66177 -1.40739,2.19644 -0.72103,0.3951 -1.48929,0.73257 -1.53088,1.60897 -0.81354,-0.32254 0.12422,1.14184 0.65017,1.27543 0.43315,0.20734 0.91104,0.44698 0.50741,-0.12798 0.30174,-0.53571 1.10164,0.28036 0.4594,-0.4453 0.0479,-1.20018 1.96828,-0.76448 2.64659,-1.55994 1.00428,-0.71951 -0.0847,-1.22662 -0.85018,-1.0046 -0.50182,0.06 -1.07873,-0.43882 -0.22366,-0.39397 0.83328,-0.14122 2.91477,0.25377 1.94632,1.4131 -1.06336,0.52011 -1.39764,1.49535 -1.68824,2.5366 -0.64436,0.88909 0.0567,1.65993 0.51433,2.21278 -0.0755,1.02463 -0.9696,1.82839 -1.60005,2.58943 -0.34214,0.34256 -1.17179,0.99352 -0.4029,0.26644 0.9274,-1.03713 1.17061,-2.97085 -0.091,-3.84667 -0.84429,-0.40075 -0.98573,0.46872 -0.61694,1.00935 -0.25867,0.8 -0.87545,1.54176 -0.22698,2.35526 -0.13692,1.16852 -1.3052,0.0978 -1.98479,0.71485 -0.94554,0.26803 -0.56905,0.54435 -1.5424,0.66888 1.68588,0.55388 2.10672,-0.0925 3.78947,-0.34726 -0.64558,0.75646 -1.80506,1.10898 -2.81213,1.13009 -2.03076,-0.002 -3.80891,-1.10663 -5.61502,-1.89118 -1.12924,-0.54753 -4.82644,-1.3203 -6.13091,-1.1211 -1.74005,0.0829 -1.83717,0.0547 -3.444,0.29301 -1.26438,-0.004 -1.42104,-0.75538 -0.51486,-2.02424 3.57949,-3.11534 6.69133,-7.03177 9.59613,-10.08739 z m -1.14004,8.46391 c -6.64532,4.78894 -3.32266,2.39447 0,0 z m -1.20322,0.0301 c -1.1412,-0.63874 -0.31307,-2.07894 0.17279,-2.89173 0.89169,-1.36972 2.86985,-1.39236 4.16156,-0.61159 1.54938,1.0298 2.07366,1.03412 2.06974,1.79612 -1.4368,0.34261 -2.55154,1.41885 -3.96896,1.80186 -0.79909,0.16878 -1.64355,0.0662 -2.43562,-0.0947 z m 3.57167,-0.77942 c 1.35312,-0.0504 0.6421,-1.87946 -0.27756,-2.06232 -2.33109,-0.63103 -2.8492,0.97005 -3.34013,1.46545 0.0455,1.37625 2.35919,1.3962 3.29693,0.77529 0.10657,-0.0601 -0.10797,0.0576 0,0 z m -3.61769,-0.59687 c -5.81249,5.68643 -2.90624,2.84321 0,0 z m 4.87351,-8.29404 c -9.0615,11.21579 -4.53075,5.60789 0,0 z"
|
||||||
|
id="path3882"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<ellipse
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
|
||||||
|
id="path4583"
|
||||||
|
cx="185.16042"
|
||||||
|
cy="234.62871"
|
||||||
|
rx="0.74570084"
|
||||||
|
ry="0.51021636" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Layer 3"
|
||||||
|
style="display:inline">
|
||||||
|
<path
|
||||||
|
style="opacity:1;vector-effect:none;fill:#2cd3e3;fill-opacity:1;fill-rule:evenodd;stroke-width:0.43717846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="path884"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="7.5222836"
|
||||||
|
sodipodi:cy="13.490612"
|
||||||
|
sodipodi:rx="3.1455584"
|
||||||
|
sodipodi:ry="1.9101746"
|
||||||
|
sodipodi:start="0"
|
||||||
|
sodipodi:end="6.1045858"
|
||||||
|
sodipodi:open="true"
|
||||||
|
d="M 10.667842,13.490612 A 3.1455584,1.9101746 0 0 1 7.6626857,15.398883 3.1455584,1.9101746 0 0 1 4.3892589,13.660964 3.1455584,1.9101746 0 0 1 7.102196,11.597548 3.1455584,1.9101746 0 0 1 10.617807,13.151267"
|
||||||
|
transform="matrix(0.93916761,-0.34345916,0.51106856,0.85953995,0,0)" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
style="display:inline">
|
||||||
|
<circle
|
||||||
|
style="opacity:1;vector-effect:none;fill:#b3ff80;fill-opacity:1;fill-rule:evenodd;stroke-width:0.38550848;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
|
id="path907"
|
||||||
|
cx="13.877442"
|
||||||
|
cy="8.9755268"
|
||||||
|
r="1.5" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.9 KiB |
BIN
public/img/logos/icon_sengi.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
1123
public/img/logos/icon_tuba.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/img/logos/logo_beagle.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
public/img/logos/logo_siskin.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
465
spec/controllers/rs/oauth_controller_spec.rb
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Rs::OauthController, type: :controller do
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
|
describe "GET /rs/oauth/:useraddress" do
|
||||||
|
context "when user is signed in" do
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when username is different than current user" do
|
||||||
|
let(:other_user) { create :user, id: 23, cn: "jomokenyatta", email: "jomo@hotmail.com" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: other_user.address,
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
scope: "examples"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "logs out the users and repeats the request" do
|
||||||
|
url = new_rs_oauth_url other_user.address,
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
scope: "examples"
|
||||||
|
|
||||||
|
expect(response).to redirect_to(url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when no valid token exists" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
state: "foobar123"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 200" do
|
||||||
|
expect(response.response_code).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sets the instance variables" do
|
||||||
|
expected_scopes = %w(documents photos contacts:rw videos:r tasks/work:r)
|
||||||
|
|
||||||
|
expect(assigns["user"]).to eq(user)
|
||||||
|
expect(assigns["redirect_uri"]).to eq("https://example.com")
|
||||||
|
expect(assigns["scopes"]).to eq(expected_scopes)
|
||||||
|
expect(assigns["client_id"]).to eq("example.com")
|
||||||
|
expect(assigns["root_access_requested"]).to eq(false)
|
||||||
|
expect(assigns["state"]).to eq("foobar123")
|
||||||
|
expect(assigns["denial_url"]).to eq("https://example.com#error=access_denied&state=foobar123")
|
||||||
|
end
|
||||||
|
|
||||||
|
context "no redirect_uri" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
client_id: "https://example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 400" do
|
||||||
|
expect(response.response_code).to eq(400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "no client_id" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects with invalid_request error" do
|
||||||
|
expect(response).to redirect_to("https://example.com#error=invalid_request")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "different host for client_id and redirect_uri" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com/foobar",
|
||||||
|
client_id: "https://google.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects with invalid_client error" do
|
||||||
|
expect(response).to redirect_to("https://example.com/foobar#error=invalid_client")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when valid token already exists" do
|
||||||
|
before do
|
||||||
|
@auth = user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
client_id: "example.com", redirect_uri: "https://example.com",
|
||||||
|
expire_at: 1.day.from_now
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
after { @auth.destroy }
|
||||||
|
|
||||||
|
context "with same host for client_id and redirect_uri" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "https://example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the redirect_uri with the existing token" do
|
||||||
|
expect(response).to redirect_to("https://example.com#access_token=#{@auth.token}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with different host for client_id and redirect_uri" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://app.example.com",
|
||||||
|
client_id: "https://example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects with invalid_client error" do
|
||||||
|
expect(response).to redirect_to("https://app.example.com#error=invalid_client")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with different redirect_uri" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com/a_new_route",
|
||||||
|
client_id: "https://example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the new redirect_uri" do
|
||||||
|
expect(response).to redirect_to("https://example.com/a_new_route#access_token=#{@auth.token}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with state param given" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "https://example.com",
|
||||||
|
state: "foobar123"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the redirect_uri with token and state" do
|
||||||
|
expect(response).to redirect_to("https://example.com#access_token=#{@auth.token}&state=foobar123")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "no scope" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "https://example.com",
|
||||||
|
state: "foobar123"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the redirect_uri with an error code" do
|
||||||
|
expect(response).to redirect_to("https://example.com#error=invalid_scope&state=foobar123")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "empty scope" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "https://example.com",
|
||||||
|
state: "foobar123"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the redirect_uri with an error code" do
|
||||||
|
expect(response).to redirect_to("https://example.com#error=invalid_scope&state=foobar123")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user is not signed in" do
|
||||||
|
it "redirects to the signin page with username pre-filled" do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "documents,photos",
|
||||||
|
redirect_uri: "https://example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to redirect_to(new_user_session_path(cn: user.cn, ou: user.ou))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "root access" do
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "full" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "*:rw",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sets the instance variables" do
|
||||||
|
expect(assigns["scopes"]).to eq([":rw"])
|
||||||
|
expect(assigns["root_access_requested"]).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "read-only" do
|
||||||
|
before do
|
||||||
|
get :new, params: {
|
||||||
|
useraddress: user.address,
|
||||||
|
scope: "*:r",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sets the instance variables" do
|
||||||
|
expect(assigns["scopes"]).to eq([":r"])
|
||||||
|
expect(assigns["root_access_requested"]).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /rs/oauth/:useraddress" do
|
||||||
|
context "when user is signed in" do
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
user.remote_storage_authorizations.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when no valid token exists" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
state: "foobar123",
|
||||||
|
expire_at: 1.day.from_now
|
||||||
|
}
|
||||||
|
@auth = user.reload.remote_storage_authorizations.first
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a new token" do
|
||||||
|
expect(@auth.permissions).to eq(%w(documents photos contacts:rw videos:r tasks/work:r))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the redirect_uri" do
|
||||||
|
expect(response).to redirect_to("https://example.com#access_token=#{@auth.token}&state=foobar123")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when token is expired" do
|
||||||
|
before do
|
||||||
|
@auth = user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(documents),
|
||||||
|
client_id: "example.com",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
expire_at: 1.day.ago,
|
||||||
|
token: nil
|
||||||
|
)
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
state: "foobar123",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates the token" do
|
||||||
|
expect(@auth.reload.token).not_to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "root access with several scopes" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "*:rw contacts:r",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
@auth = user.reload.remote_storage_authorizations.first
|
||||||
|
end
|
||||||
|
|
||||||
|
it "removes all scopes except for the root permission" do
|
||||||
|
expect(@auth.permissions).to eq(%w(:rw))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "no redirect_uri" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
client_id: "example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 400" do
|
||||||
|
expect(response.response_code).to eq(400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "no client_id" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects with invalid_request error" do
|
||||||
|
expect(response).to redirect_to("https://example.com#error=invalid_request")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "hostnames of client_id and redirect_uri do not match" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents,[photos], contacts:rw videos:r tasks/work/:r",
|
||||||
|
client_id: "fishing.com",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects with invalid_client error" do
|
||||||
|
expect(response).to redirect_to("https://example.com#error=invalid_client")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "empty scope" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
state: "foobar123",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the redirect_uri with an error code" do
|
||||||
|
expect(response).to redirect_to("https://example.com#error=invalid_scope&state=foobar123")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the user_id is different from the signed in user" do
|
||||||
|
before do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents,photos",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 403" do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: "69",
|
||||||
|
scope: "documents,photos",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.response_code).to eq(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user is not signed in" do
|
||||||
|
it "redirects to the signin page" do
|
||||||
|
post :create, params: {
|
||||||
|
user_id: user.id,
|
||||||
|
scope: "documents,photos",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
client_id: "example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /rs/oauth/token/:id/launch_app" do
|
||||||
|
context "when user is signed in" do
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
context "token exists" do
|
||||||
|
before do
|
||||||
|
@auth = user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(documents), client_id: "app.example.com",
|
||||||
|
redirect_uri: "https://app.example.com",
|
||||||
|
expire_at: 2.days.from_now
|
||||||
|
)
|
||||||
|
|
||||||
|
get :launch_app, params: { id: @auth.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
@auth.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "redirects to the given URL with the correct RS URL fragment params" do
|
||||||
|
launch_url = "https://app.example.com#remotestorage=#{user.address}&access_token=#{@auth.token}"
|
||||||
|
expect(response).to redirect_to(launch_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
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
|
||||||
|
|||||||
66
spec/features/rs/oauth_spec.rb
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'remoteStorage OAuth Dialog', type: :feature do
|
||||||
|
context "when signed in" do
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
|
before do
|
||||||
|
login_as user, :scope => :user
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with normal permissions" do
|
||||||
|
before do
|
||||||
|
visit new_rs_oauth_path(useraddress: user.address,
|
||||||
|
redirect_uri: "http://example.com",
|
||||||
|
client_id: "http://example.com",
|
||||||
|
scope: "documents,[photos], contacts:r")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows the permissions in a list" do
|
||||||
|
within ".permissions" do
|
||||||
|
expect(page).to have_content("documents")
|
||||||
|
expect(page).to have_content("photos")
|
||||||
|
expect(page).to have_content("contacts")
|
||||||
|
end
|
||||||
|
|
||||||
|
within ".scope:first-of-type" do
|
||||||
|
expect(page).not_to have_content("read only")
|
||||||
|
end
|
||||||
|
|
||||||
|
within ".scope:last-of-type" do
|
||||||
|
expect(page).to have_content("read only")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "root access" do
|
||||||
|
context "full" do
|
||||||
|
before do
|
||||||
|
visit new_rs_oauth_path(useraddress: user.address,
|
||||||
|
redirect_uri: "http://example.com",
|
||||||
|
client_id: "http://example.com",
|
||||||
|
scope: ":rw")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "shows a special permission for all files and dirs" do
|
||||||
|
within ".scope" do
|
||||||
|
expect(page).to have_content("All files and directories")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when signed out" do
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
|
it "prefills the username field in the signin form" do
|
||||||
|
visit new_rs_oauth_path(useraddress: user.address,
|
||||||
|
redirect_uri: "http://example.com",
|
||||||
|
client_id: "http://example.com",
|
||||||
|
scope: "documents,[photos], contacts:r")
|
||||||
|
|
||||||
|
expect(find("#user_cn").value).to eq(user.cn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
36
spec/jobs/remote_storage_expire_authorization_job_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RemoteStorageExpireAuthorizationJob, 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.rs_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
|
||||||
212
spec/models/remote_storage_authorization_spec.rb
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RemoteStorageAuthorization, type: :model do
|
||||||
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
|
describe "#create" do
|
||||||
|
after(:each) { clear_enqueued_jobs }
|
||||||
|
after(:all) { redis_rs_delete_keys("rs:authorizations:*") }
|
||||||
|
|
||||||
|
let(:auth) do
|
||||||
|
user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
client_id: "example.com",
|
||||||
|
redirect_uri: "https://example.com"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "generates a token" do
|
||||||
|
expect(auth.token).to match(/[a-zA-Z0-9]+/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "stores a token in redis" do
|
||||||
|
user_auth_keys = redis_rs.keys("rs:authorizations:#{user.address}:*")
|
||||||
|
expect(user_auth_keys.length).to eq(1)
|
||||||
|
|
||||||
|
authorizations = redis_rs.smembers(user_auth_keys.first)
|
||||||
|
expect(authorizations.sort).to eq(%w(documents photos contacts:rw videos:r tasks/work:r).sort)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with expiry set" do
|
||||||
|
it "enqueues an expiration job" do
|
||||||
|
auth_with_expiry = user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(documents:rw), client_id: "example.com",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
)
|
||||||
|
job = enqueued_jobs.select{|j| j['job_class'] == "RemoteStorageExpireAuthorizationJob"}.first
|
||||||
|
expect(job['arguments'][0]).to eq(auth_with_expiry.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#destroy" do
|
||||||
|
after(:each) { clear_enqueued_jobs }
|
||||||
|
after(:all) { redis_rs_delete_keys("rs:authorizations:*") }
|
||||||
|
|
||||||
|
it "removes the token from redis" do
|
||||||
|
auth = user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(shares:rw documents pictures:r),
|
||||||
|
client_id: "sharesome.5apps.com",
|
||||||
|
redirect_uri: "https://sharesome.5apps.com"
|
||||||
|
)
|
||||||
|
auth.destroy!
|
||||||
|
|
||||||
|
expect(redis_rs.keys("rs:authorizations:#{user.address}:*")).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with expiry set" do
|
||||||
|
it "removes the expiration job" do
|
||||||
|
auth_with_expiry = user.remote_storage_authorizations.create!(
|
||||||
|
permissions: %w(documents:rw), client_id: "example.com",
|
||||||
|
redirect_uri: "https://example.com",
|
||||||
|
expire_at: 1.month.from_now
|
||||||
|
)
|
||||||
|
# Cannot test for removal from the actual Sidekiq::Queue, because it is
|
||||||
|
# not used in specs, but the method directly removes jobs from there
|
||||||
|
expect(auth_with_expiry).to receive(:remove_token_expiry_job)
|
||||||
|
auth_with_expiry.destroy!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# describe "#find_or_create_web_app" do
|
||||||
|
# context "with origin that looks hosted" do
|
||||||
|
# before do
|
||||||
|
# auth = user.remote_storage_authorizations.create!(
|
||||||
|
# permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
# client_id: "example.com",
|
||||||
|
# redirect_uri: "https://example.com",
|
||||||
|
# expire_at: 1.month.from_now
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "generates a web_app" do
|
||||||
|
# expect(auth.web_app).to be_a(AppCatalog::WebApp)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "uses the Web App's name as app name" do
|
||||||
|
# expect(auth.app_name).to eq("Example Domain")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# context "when creating two authorizations for the same app" do
|
||||||
|
# before do
|
||||||
|
# user_2 = create :user
|
||||||
|
# ResqueSpec.reset!
|
||||||
|
# auth_1 = user.remote_storage_authorizations.create!(
|
||||||
|
# permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
# client_id: "example.com",
|
||||||
|
# redirect_uri: "https://example.com",
|
||||||
|
# expire_at: 1.month.from_now
|
||||||
|
# )
|
||||||
|
# auth_2 = user_2.remote_storage_authorizations.create!(
|
||||||
|
# permissions: %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
# client_id: "example.com",
|
||||||
|
# redirect_uri: "https://example.com",
|
||||||
|
# expire_at: 1.month.from_now
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# after do
|
||||||
|
# auth_1.destroy
|
||||||
|
# auth_2.destroy
|
||||||
|
# user_2.destroy
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "uses the same web app instance for both authorizations" do
|
||||||
|
# expect(auth_1.web_app).to be_a(AppCatalog::WebApp)
|
||||||
|
# expect(auth_1.web_app).to eq(auth_2.web_app)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "non-production app origins" do
|
||||||
|
# context "when host is not an FQDN" do
|
||||||
|
# before do
|
||||||
|
# auth = user.remote_storage_authorizations.create!(
|
||||||
|
# permissions: %w(recipes),
|
||||||
|
# client_id: "localhost:4200",
|
||||||
|
# redirect_uri: "http://localhost:4200"
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "does not create a web app" do
|
||||||
|
# expect(auth.web_app).to be_nil
|
||||||
|
# expect(auth.app_name).to eq("localhost:4200")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# context "when host is an IP address" do
|
||||||
|
# before do
|
||||||
|
# auth = user.remote_storage_authorizations.create!(
|
||||||
|
# permissions: %w(recipes),
|
||||||
|
# client_id: "192.168.0.23:3000",
|
||||||
|
# redirect_uri: "http://192.168.0.23:3000"
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "does not create a web app" do
|
||||||
|
# expect(auth.web_app).to be_nil
|
||||||
|
# expect(auth.app_name).to eq("192.168.0.23:3000")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# context "when host is an extension URL" do # before do
|
||||||
|
# auth = user.remote_storage_authorizations.create!(
|
||||||
|
# permissions: %w(bookmarks),
|
||||||
|
# client_id: "123.addons.allizom.org",
|
||||||
|
# redirect_uri: "123.addons.allizom.org/foo"
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "does not create a web app" do
|
||||||
|
# expect(auth.web_app).to be_nil
|
||||||
|
# expect(auth.app_name).to eq("123.addons.allizom.org")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# describe "auth notifications" do
|
||||||
|
# context "with auth notifications enabled" do
|
||||||
|
# before do
|
||||||
|
# ResqueSpec.reset!
|
||||||
|
# user.push(mailing_lists: "rs-auth-notifications-#{Rails.env}")
|
||||||
|
# auth = user.remote_storage_authorizations.create!(
|
||||||
|
# :permissions => %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
# :client_id => "example.com",
|
||||||
|
# :redirect_uri => "https://example.com"
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "notifies the user via email" do
|
||||||
|
# expect(enqueued_jobs.size).to eq(1)
|
||||||
|
# job = enqueued_jobs.first
|
||||||
|
# expect(job).to eq(
|
||||||
|
# job: ActionMailer::DeliveryJob,
|
||||||
|
# args: ['StorageAuthorizationMailer', 'authorized_rs_app', 'deliver_now',
|
||||||
|
# auth.id.to_s],
|
||||||
|
# queue: 'mailers'
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# context "with auth notifications disabled" do
|
||||||
|
# before do
|
||||||
|
# ResqueSpec.reset!
|
||||||
|
# user.pull(mailing_lists: "rs-auth-notifications-#{Rails.env}")
|
||||||
|
# auth = user.remote_storage_authorizations.create!(
|
||||||
|
# :permissions => %w(documents photos contacts:rw videos:r tasks/work:r),
|
||||||
|
# :client_id => "example.com",
|
||||||
|
# :redirect_uri => "https://example.com"
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# it "does not notify the user via email about new RS app" do
|
||||||
|
# expect(enqueued_jobs.size).to eq(0)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
end
|
||||||
@@ -12,6 +12,38 @@ RSpec.describe User, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#mastodon_address" do
|
||||||
|
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
|
||||||
|
|
||||||
|
context "Mastodon service not configured" do
|
||||||
|
it "returns nil" do
|
||||||
|
expect(user.mastodon_address).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Mastodon service configured" do
|
||||||
|
before do
|
||||||
|
Setting.mastodon_enabled = true
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "domain is the same as primary domain" do
|
||||||
|
it "returns the user address" do
|
||||||
|
expect(user.mastodon_address).to eq("jimmy@kosmos.org")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "domain is different from primary domain" do
|
||||||
|
before do
|
||||||
|
Setting.mastodon_address_domain = "kosmos.social"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the user address" do
|
||||||
|
expect(user.mastodon_address).to eq("jimmy@kosmos.social")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#is_admin?" do
|
describe "#is_admin?" do
|
||||||
it "returns true when admin flag is set in LDAP" do
|
it "returns true when admin flag is set in LDAP" do
|
||||||
expect(Devise::LDAP::Adapter).to receive(:get_ldap_param)
|
expect(Devise::LDAP::Adapter).to receive(:get_ldap_param)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ require 'capybara'
|
|||||||
require 'devise'
|
require 'devise'
|
||||||
require 'support/controller_macros'
|
require 'support/controller_macros'
|
||||||
require 'support/database_cleaner'
|
require 'support/database_cleaner'
|
||||||
|
require 'support/helpers/redis_helper'
|
||||||
require "view_component/test_helpers"
|
require "view_component/test_helpers"
|
||||||
require "capybara/rspec"
|
require "capybara/rspec"
|
||||||
|
|
||||||
|
|||||||
8
spec/support/helpers/redis_helper.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
def redis_rs
|
||||||
|
@redis_rs ||= Redis.new(url: Setting.rs_redis_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis_rs_delete_keys(pattern)
|
||||||
|
keys = redis_rs.keys(pattern)
|
||||||
|
redis_rs.del(*keys)
|
||||||
|
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',
|
||||||
|
|||||||