57 Commits

Author SHA1 Message Date
Râu Cao
6739b38f4c 0.8.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-01 12:18:26 +02:00
7e1272c936 Merge pull request 'Service pages for Chat and Social' (#143) from feature/service_pages into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #143
Reviewed-by: galfert <garret.alfert@gmail.com>
2023-09-01 08:36:09 +00:00
Râu Cao
ecdeb4c122 Fix copypasta
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-09-01 10:32:11 +02:00
Râu Cao
8614e2f12b Use service configs on dashboard
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Only show enabled services, and use the URLs from the various configs.
2023-08-13 17:24:10 +02:00
Râu Cao
a038a857d9 Make Drone CI configurable 2023-08-13 17:23:57 +02:00
Râu Cao
eee81d0cf1 Small link improvement
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-13 15:41:57 +02:00
Râu Cao
b7fa4b012a Allow Mastodon address domain to be different from primary domain
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-13 12:27:05 +02:00
Râu Cao
10bcd5c32b Ignore .env.development 2023-08-13 12:26:56 +02:00
Râu Cao
f79d5d4724 Use select element instead of tabs on mobile
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-11 14:17:12 +02:00
Râu Cao
866ffbe615 Upgrade tailwindcss-stimulus-components to latest version
All checks were successful
continuous-integration/drone/push Build is passing
The latest one offers more tabs features. Required some changes to the
modals and tabs code.
2023-08-11 13:58:57 +02:00
Râu Cao
3c1fe3396d Add Mastodon service page 2023-08-11 13:58:53 +02:00
Râu Cao
e4242333d9 Add recommended apps for Chat/XMPP
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-08 19:59:29 +02:00
Râu Cao
138f13c1a0 Add note
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-07 18:16:40 +02:00
Râu Cao
ad5e515200 Update README 2023-08-07 18:16:34 +02:00
Râu Cao
1ea8b22a59 WIP Add service page for Chat
Some checks are pending
continuous-integration/drone/push Build is running
2023-08-07 18:16:14 +02:00
Râu Cao
f49aff262c Add base controller for service controllers
Some checks are pending
continuous-integration/drone/push Build is running
2023-08-07 18:15:17 +02:00
852e2fea1e Merge pull request 'remoteStorage OAuth' (#109) from feature/rs-oauth into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #109
2023-08-04 08:55:28 +00:00
Râu Cao
353b55fe1a Add RS OAuth controller specs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-08-01 14:29:24 +02:00
Râu Cao
ba0cbba96b Add feature spec for RS OAuth dialog
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-01 13:01:41 +02:00
Râu Cao
5f921f1b53 RS OAuth pre-fills username for login 2023-08-01 13:01:03 +02:00
Râu Cao
a2d27bf575 Support pre-filling of username in login form 2023-08-01 13:00:22 +02:00
Râu Cao
fcf9a065e1 Fix specs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-07-14 15:56:28 +02:00
Râu Cao
ec9bcacd46 Add specs for RemoteStorageAuthorization model
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-07-14 15:31:20 +02:00
Râu Cao
645abac810 Rename RS token expiry job 2023-07-14 15:29:29 +02:00
Râu Cao
e11be727a1 Indentation 2023-07-14 15:29:04 +02:00
Râu Cao
12b24337e7 Fix typo 2023-07-14 15:28:45 +02:00
Râu Cao
b0bfc290c4 Refactor code for newer Redis 2023-07-14 15:28:09 +02:00
Râu Cao
4c6c81171b Fix typo 2023-07-14 15:27:57 +02:00
Râu Cao
4d88a40109 Add separate config for RS Redis 2023-07-14 15:27:30 +02:00
Râu Cao
d9b39b36fb Merge branch 'master' into feature/rs-oauth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-07-13 15:31:28 +02:00
Râu Cao
06aed8c33d Count up minor version on feature merge
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-13 15:26:35 +02:00
0a778e92d8 Merge pull request 'Add modal component, QR codes for invite links' (#140) from feature/139-qr_codes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #140
2023-07-13 13:24:12 +00:00
Râu Cao
e5a5633e44 Add Redis config for dev with Redis on localhost
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-07-04 17:00:04 +02:00
Râu Cao
a68825493f Add Redis config in CI
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-07-04 16:44:11 +02:00
Râu Cao
e1e83386a8 Merge branch 'master' into feature/rs-oauth 2023-07-04 16:43:32 +02:00
Râu Cao
3adc1917f6 Improve outline button style, use everywhere
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2023-06-27 19:23:20 +02:00
Râu Cao
8a570ce724 Use modal component for LndHub setup code 2023-06-27 19:23:20 +02:00
Râu Cao
c78df9e5f1 Add QR code icon, button, modal for invites
Using https://excid3.github.io/tailwindcss-stimulus-components/
2023-06-27 19:23:20 +02:00
5c2df3df07 Add Redis service to Drone config
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-06-27 15:07:28 +02:00
83e3e2ecd8 Merge pull request 'Allow editing and resetting of all admin setting strings' (#137) from feature/admin_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #137
Reviewed-by: greg <greg@noreply.kosmos.org>
2023-06-24 14:57:08 +00:00
b32e2fcb7b Merge pull request 'Fix docker volume mappings for node_modules, improve docker-compose usage' (#138) from bugfix/fix-docker-mapping into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #138
Reviewed-by: raucao <raucao@noreply.kosmos.org>
2023-06-23 15:38:30 +00:00
96a4db5bae improve sentence
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 4s
2023-06-23 17:32:39 +02:00
c7925f132e formatting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-06-23 17:30:00 +02:00
e4406bf6ff use PRIMARY_DOMAIN for both web and sidekiq directives 2023-06-23 17:29:42 +02:00
ee7769c8c7 Update readme with simplified usage
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-06-23 17:21:52 +02:00
fdf3218f88 leave services uncommented, add /akkounts/node_modules to volume mapping as a directory to explicitly exclude 2023-06-23 17:21:43 +02:00
652ed5f7e3 copy files as list 2023-06-23 17:21:17 +02:00
Râu Cao
e4ed797920 Adjust specs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2023-06-22 13:57:55 +02:00
Râu Cao
93740f17ef Allow editing and resetting of all admin setting strings
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-06-22 13:48:29 +02:00
Râu Cao
affb058671 Add config for XMPP notifications from-address 2023-06-21 16:44:06 +02:00
716d4b944a Merge branch 'master' into feature/rs-oauth
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
# Conflicts:
#	app/models/user.rb
#	config/routes.rb
#	db/schema.rb
2023-06-20 14:07:46 +02:00
42af148168 Persist RS auth tokens in Redis 2023-06-20 14:02:48 +02:00
Râu Cao
dabd892a25 Improve RS OAuth UI
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-13 16:21:48 +02:00
Râu Cao
eeabbdb7df Merge branch 'master' into feature/rs-oauth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-04-13 15:02:51 +02:00
ee42d68471 Add RemoteStorageAuthorization model
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-03-28 01:21:28 +02:00
7acc3b2106 RemoteStorage OAuth dialog 2023-03-28 01:21:28 +02:00
20c014607c Basic RemoteStorage settings 2023-03-27 22:52:01 +02:00
91 changed files with 3544 additions and 310 deletions

View File

@@ -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:

View File

@@ -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'

View File

@@ -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'

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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";

View 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);
}

View File

@@ -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 {

View 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>

View 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

View File

@@ -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">

View File

@@ -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

View File

@@ -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 %>

View File

@@ -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

View 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>

View File

@@ -0,0 +1,2 @@
class ModalComponent < ViewComponent::Base
end

View 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 %>

View 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

View 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

View File

@@ -0,0 +1,9 @@
class Services::BaseController < ApplicationController
before_action :set_current_section
private
def set_current_section
@current_section = :services
end
end

View 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

View File

@@ -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]

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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
}
}

View 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

View 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

View File

@@ -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

View File

@@ -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?

View File

@@ -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>

View 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>

View File

@@ -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 %>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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&#45;gray&#45;300 rounded&#45;md hover:border&#45;gray&#45;400 --> <% if Setting.mediawiki_enabled? %>
<!-- bg&#45;[length:80%] bg&#45;[right_top_&#45;30px] bg&#45;no&#45;repeat --> <div class="border border-gray-300 rounded-md hover:border-gray-400
<!-- bg&#45;[url(/img/logos/icon_mastodon.svg)]"> --> bg-cover bg-[center_top_-20px] bg-no-repeat
<!-- <%= link_to "https://kosmos.social", class: "block h&#45;full px&#45;6 py&#45;6 rounded&#45;md" do %> --> bg-[url(/img/logos/icon_mediawiki.svg)]">
<!-- <h3 class="mb&#45;3.5">Mastodon</h3> --> <%= link_to Setting.mediawiki_public_url,
<!-- <p class="text&#45;gray&#45;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 %>

View File

@@ -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: {

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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>

View 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 %>

View 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&#45;2" data&#45;tabs&#45;target="tab" data&#45;action="click&#45;>tabs#change"> -->
<!-- <a href="#" class="bg&#45;white inline&#45;block py&#45;2 px&#45;4 font&#45;semibold no&#45;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&#45;cols&#45;1 gap&#45;4 sm:gap&#45;6" data&#45;tabs&#45;target="panel"> -->
<!-- Web -->
<!-- </div> -->
</div>
</section>
<% end %>

View File

@@ -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>

View 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 %>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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",

View 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 %>

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View 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

View 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

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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)

View File

@@ -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"

View 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

View File

@@ -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',

File diff suppressed because one or more lines are too long