diff --git a/app/aggregates/member.rb b/app/aggregates/member.rb index e2bcacd..48c1bef 100644 --- a/app/aggregates/member.rb +++ b/app/aggregates/member.rb @@ -22,10 +22,9 @@ def initialize(id, events) end apply MemberAdded do |event| - username = event.body['username'] write_attributes( added: true, - handle: Handle.new(username), + handle: event.body['handle'], email: event.body['email'], name: event.body['name'] ) diff --git a/app/aggregates/peer.rb b/app/aggregates/peer.rb new file mode 100644 index 0000000..3b8f493 --- /dev/null +++ b/app/aggregates/peer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Aggregates + ## + # A +Peer+ is a remote actor. + class Peer + include EventSourcery::AggregateRoot + + AWAIT_SYNCING_VALUE = 'Synching...' + + attr_reader :name, :bio + + def initialize(id, events) + @name = AWAIT_SYNCING_VALUE + @bio = AWAIT_SYNCING_VALUE + super(id, events) + end + + apply PeerSynched do + end + end +end diff --git a/app/aggregates/registration.rb b/app/aggregates/registration.rb index 1e08383..e82f603 100644 --- a/app/aggregates/registration.rb +++ b/app/aggregates/registration.rb @@ -28,7 +28,7 @@ def initialize(*arguments) end apply RegistrationRequested do |event| - write_attributes(event.body.slice('username', 'password', 'email')) + write_attributes(event.body.slice('handle', 'password', 'email')) end apply RegistrationConfirmed do @@ -69,8 +69,8 @@ def password attributes[:password] end - def username - attributes[:username] + def handle + attributes[:handle] end end end diff --git a/app/commands/application_command.rb b/app/commands/application_command.rb index 7228b74..217183d 100644 --- a/app/commands/application_command.rb +++ b/app/commands/application_command.rb @@ -23,10 +23,7 @@ def uuid_v5 if aggregate_id_name.empty? '' else - UUIDTools::UUID.sha1_create( - aggregate_id_namespace, - aggregate_id_name - ).to_s + UUIDGen.uuid(aggregate_id_namespace, aggregate_id_name).to_s end end diff --git a/app/commands/registration/new_registration.rb b/app/commands/registration/new_registration.rb index cd053f0..2ca32db 100644 --- a/app/commands/registration/new_registration.rb +++ b/app/commands/registration/new_registration.rb @@ -11,11 +11,7 @@ module NewRegistration class Command < ApplicationCommand include BCrypt - UUID_EMAIL_NAMESPACE = UUIDTools::UUID.parse( - '2282b78c-85d6-419f-b240-0263d67ee6e6' - ) - - REQUIRED_PARAMS = %w[email username password].freeze + REQUIRED_PARAMS = %w[email handle password].freeze ALLOWED_PARAMS = REQUIRED_PARAMS # NewRegistration builds a UUIDv5 based on the mailaddress. @@ -23,7 +19,9 @@ def initialize(params) @payload = params.slice(*ALLOWED_PARAMS) # overwrite the password - @payload['password'] = Password.create(@payload.delete('password')) + unless (@payload['password'] || '').empty? + @payload['password'] = Password.create(@payload.delete('password')) + end @aggregate_id = aggregate_id end @@ -47,7 +45,7 @@ def aggregate_id_name end def aggregate_id_namespace - UUID_EMAIL_NAMESPACE + UUIDGen::NS_EMAIL end end diff --git a/app/commands/session/start.rb b/app/commands/session/start.rb index 3d6714b..7018d9a 100644 --- a/app/commands/session/start.rb +++ b/app/commands/session/start.rb @@ -13,10 +13,7 @@ module Start # with e.g. a rate limiter, or notification mail or so. class Command < ApplicationCommand include BCrypt - UUID_USERNAME_NAMESPACE = UUIDTools::UUID.parse( - 'fb0f6f73-a16d-4032-b508-16519fb4a73a' - ) - DEFAULT_PARAMS = { 'username' => '', 'password' => '' }.freeze + DEFAULT_PARAMS = { 'handle' => '', 'password' => '' }.freeze def initialize(params, projection: Projections::Members::Query) @payload = DEFAULT_PARAMS.merge(params).slice(*DEFAULT_PARAMS.keys) @@ -47,15 +44,15 @@ def payload attr_reader :projection def aggregate_id_name - @payload['username'] + @payload['handle'] end def aggregate_id_namespace - UUID_USERNAME_NAMESPACE + UUIDGen::NS_USERNAME end def member - @member ||= projection.find_by(username: @payload['username']) || {} + @member ||= projection.find_by(handle: @payload['handle']) || {} end end diff --git a/app/events/confirmation_email_sent.rb b/app/events/confirmation_email_sent.rb deleted file mode 100644 index caa0586..0000000 --- a/app/events/confirmation_email_sent.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A confirmation email has been sent -ConfirmationEmailSent = Class.new(EventSourcery::Event) diff --git a/app/events/contact_added.rb b/app/events/contact_added.rb deleted file mode 100644 index 1ed6d31..0000000 --- a/app/events/contact_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Contact was added to a Member -ContactAdded = Class.new(EventSourcery::Event) diff --git a/app/events/events.rb b/app/events/events.rb new file mode 100644 index 0000000..bc88a04 --- /dev/null +++ b/app/events/events.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +## +# A confirmation email has been sent +ConfirmationEmailSent = Class.new(EventSourcery::Event) + +## +# A Contact was added to a Member +# +aggregate_id+, UUID of the contact aggregate +# +body.owner_id+, UUID of the member owning the contact: the contact shows +# up in this member's contacts +# +body.handle+, Handle, the handle of the contact to be added. +ContactAdded = Class.new(EventSourcery::Event) + +## +# A follower was added to a member +FollowerAdded = Class.new(EventSourcery::Event) + +## +# A Member was added +MemberAdded = Class.new(EventSourcery::Event) + +## +# A profile bio was updated +MemberBioUpdated = Class.new(EventSourcery::Event) + +## +# A Member was invited +MemberInvited = Class.new(EventSourcery::Event) + +## +# A profile bio was updated +MemberNameUpdated = Class.new(EventSourcery::Event) + +## +# A Member was Tagged +MemberTagAdded = Class.new(EventSourcery::Event) + +## +# Fetching of Details for peer from remote server requested. +PeerFetchRequested = Class.new(EventSourcery::Event) + +## +# Fetching of Details for peer finished +PeerSynched = Class.new(EventSourcery::Event) + +## +# A Registration is confirmed +RegistrationConfirmed = Class.new(EventSourcery::Event) + +## +# A visitor has registered: reguested a new registration +RegistrationRequested = Class.new(EventSourcery::Event) + +## +# A Logged in session was started +SessionStarted = Class.new(EventSourcery::Event) diff --git a/app/events/follower_added.rb b/app/events/follower_added.rb deleted file mode 100644 index 0839c76..0000000 --- a/app/events/follower_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A follower was added to a member -FollowerAdded = Class.new(EventSourcery::Event) diff --git a/app/events/member_added.rb b/app/events/member_added.rb deleted file mode 100644 index 23b693e..0000000 --- a/app/events/member_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Member was added -MemberAdded = Class.new(EventSourcery::Event) diff --git a/app/events/member_bio_updated.rb b/app/events/member_bio_updated.rb deleted file mode 100644 index 879146c..0000000 --- a/app/events/member_bio_updated.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A profile bio was updated -MemberBioUpdated = Class.new(EventSourcery::Event) diff --git a/app/events/member_invited.rb b/app/events/member_invited.rb deleted file mode 100644 index 9945826..0000000 --- a/app/events/member_invited.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -## -# A Member was invited -class MemberInvited < EventSourcery::Event -end diff --git a/app/events/member_name_updated.rb b/app/events/member_name_updated.rb deleted file mode 100644 index d19a27c..0000000 --- a/app/events/member_name_updated.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A profile bio was updated -MemberNameUpdated = Class.new(EventSourcery::Event) diff --git a/app/events/member_tag_added.rb b/app/events/member_tag_added.rb deleted file mode 100644 index 928d424..0000000 --- a/app/events/member_tag_added.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Member was Tagged -MemberTagAdded = Class.new(EventSourcery::Event) diff --git a/app/events/registration_confirmed.rb b/app/events/registration_confirmed.rb deleted file mode 100644 index 52c640f..0000000 --- a/app/events/registration_confirmed.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Registration is confirmed -RegistrationConfirmed = Class.new(EventSourcery::Event) diff --git a/app/events/registration_requested.rb b/app/events/registration_requested.rb deleted file mode 100644 index 1fe8c82..0000000 --- a/app/events/registration_requested.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A visitor has registered: reguested a new registration -RegistrationRequested = Class.new(EventSourcery::Event) diff --git a/app/events/session_started.rb b/app/events/session_started.rb deleted file mode 100644 index e394b52..0000000 --- a/app/events/session_started.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -## -# A Logged in session was started -SessionStarted = Class.new(EventSourcery::Event) diff --git a/app/projections/members/projector.rb b/app/projections/members/projector.rb index 5f0e7e3..b04ba64 100644 --- a/app/projections/members/projector.rb +++ b/app/projections/members/projector.rb @@ -12,17 +12,13 @@ class Projector table :members do column :member_id, 'UUID NOT NULL' column :handle, :text, null: false - column :username, :text column :password, :text end project MemberAdded do |event| - username = event.body['username'] - table.insert( member_id: event.aggregate_id, - handle: Handle.new(username).to_s, - username: username, + handle: event.body['handle'], password: event.body['password'] ) end diff --git a/app/projections/members/query.rb b/app/projections/members/query.rb index 6a2edc7..8124504 100644 --- a/app/projections/members/query.rb +++ b/app/projections/members/query.rb @@ -5,8 +5,8 @@ module Members # Query the Members projection with helpers that return # Sequel collection objects. class Query - def self.find_by(username:) - collection.first(username: username) + def self.find_by(handle:) + collection.first(handle: handle) end def self.find(id) diff --git a/app/reactors/contact_fetcher.rb b/app/reactors/contact_fetcher.rb new file mode 100644 index 0000000..1cbfd02 --- /dev/null +++ b/app/reactors/contact_fetcher.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Reactors + ## + # Adds an actor to a members' followers on various Events + class ContactFetcher + include EventSourcery::Postgres::Reactor + + processor_name :contact_fetcher + + emits_events PeerSynched + + process PeerFetchRequested do |event| + emit_event( + PeerSynched.new( + aggregate_id: event.aggregate_id, + body: event.body, + causation_id: event.uuid + ) + ) + end + end +end diff --git a/app/web/controllers/application_controller.rb b/app/web/controllers/application_controller.rb index 11d1537..f4d908f 100644 --- a/app/web/controllers/application_controller.rb +++ b/app/web/controllers/application_controller.rb @@ -19,6 +19,12 @@ class ApplicationController < Sinatra::Base # :nocov: end + helpers do + def domain + Roost.config.domain + end + end + protected def requires_authorization @@ -29,6 +35,10 @@ def authorize(&block) raise Unauthorized unless block.call end + def authorized? + current_member.active? + end + def current_member return OpenStruct.new(active?: false) unless member_id diff --git a/app/web/controllers/web/contacts_controller.rb b/app/web/controllers/web/contacts_controller.rb index 24ccc38..deb61a0 100644 --- a/app/web/controllers/web/contacts_controller.rb +++ b/app/web/controllers/web/contacts_controller.rb @@ -4,6 +4,7 @@ module Web ## # Handles contacts index and addition class ContactsController < WebController + include LoadHelpers include PolicyHelpers # Index @@ -16,27 +17,34 @@ class ContactsController < WebController # Add post '/contacts' do - requires_authorization + redirect '/remote' unless authorized? + authorize { may_add_contact? } Commands.handle( 'Contact', 'Add', - { 'handle' => contact.handle, 'owner_id' => current_member.member_id } + { 'handle' => handle, 'owner_id' => current_member.member_id } ) - flash[:success] = "#{contact.handle} was added to your contacts" - redirect "/m/#{contact.handle}" + flash[:success] = "#{handle} was added to your contacts" + redirect "/m/#{handle}" end private def contact - aggregate_id = Projections::Members::Query.aggregate_id_for(handle) - Roost.repository.load(Aggregates::Member, aggregate_id) + decorate( + load( + Aggregates::Member, + Projections::Members::Query.aggregate_id_for(handle.to_s) + ), + ViewModels::Profile, + ViewModels::Profile::NullProfile + ) end def handle - Handle.parse(params['handle']) + Handle.parse(params[:handle]) end end end diff --git a/app/web/controllers/web/login_controller.rb b/app/web/controllers/web/login_controller.rb index 171f866..791faf1 100644 --- a/app/web/controllers/web/login_controller.rb +++ b/app/web/controllers/web/login_controller.rb @@ -16,7 +16,9 @@ class LoginController < WebController private def post_params - params.slice('username', 'password') + post_params = params.slice(:password) + post_params[:handle] = Handle.new(params[:username]).to_s + post_params end end end diff --git a/app/web/controllers/web/profiles_controller.rb b/app/web/controllers/web/profiles_controller.rb index 2015025..fc2f78b 100644 --- a/app/web/controllers/web/profiles_controller.rb +++ b/app/web/controllers/web/profiles_controller.rb @@ -10,6 +10,10 @@ class ProfilesController < WebController # TODO: /@handle should redirect to /@handle@example.org when we are # on example.org # TODO: /handle should redirect to /@handle@example.org as well + # TODO: redirect to remote host if handle is note remote. E.g. + # if we are on example.com and get a @handle@example.org we should redirect + # to example.org/m/handle@example.org. But for that we need remote + # canonical location determination (e.g. webfinger) first. get '/m/:handle' do raise NotFound, 'Member with that handle not found' if profile.null? diff --git a/app/web/controllers/web/registrations_controller.rb b/app/web/controllers/web/registrations_controller.rb index 270b74f..9b558bb 100644 --- a/app/web/controllers/web/registrations_controller.rb +++ b/app/web/controllers/web/registrations_controller.rb @@ -20,7 +20,9 @@ class RegistrationsController < WebController private def post_params - params.slice('username', 'password', 'email') + post_params = params.slice(:password, :email) + post_params[:handle] = Handle.new(params[:username]).to_s + post_params end end end diff --git a/app/web/controllers/web/remote_confirmations_controller.rb b/app/web/controllers/web/remote_confirmations_controller.rb new file mode 100644 index 0000000..623333c --- /dev/null +++ b/app/web/controllers/web/remote_confirmations_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Web + ## + # Handles incoming redirects from remotes to confirm an action + class RemoteConfirmationsController < WebController + # TODO: authenticate and authorize + get '/remote_confirmation' do + erb( + :remote_confirmation, + layout: :layout_member, + locals: { + message: message, + form_action: form_action, + target: target, + taget_type: target_type + } + ) + end + + private + + # TODO: Once we have more actions, fetch this from signed attributes and + # pull through an allowlist + def form_action + 'contacts' + end + + # TODO: Unhardcode this message + def message + "As @harry@example.com you want to follow #{target}" + end + + # TODO: Fetch the target from attributes + def target + '@luna@ravenclaw.example.org' + end + + # TODO: Once we have more target types, fetch this from signed attributes + # and pull through an allowlist + def target_type + 'account' + end + end +end diff --git a/app/web/controllers/web/remote_controller.rb b/app/web/controllers/web/remote_controller.rb new file mode 100644 index 0000000..ba254cf --- /dev/null +++ b/app/web/controllers/web/remote_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Web + ## + # Handles Remote redirects + # TODO: sanitize and whitelist actions + # TODO: handle misparsed handles + # TODO: exchange server-server signed secrets with remote instance so that + # remote instance knows this request is coming and can validate it. + class RemoteController < WebController + get '/remote' do + erb( + :remote, + layout: :layout_anonymous, + locals: { + message: message, + action: action, + target: target, + taget_type: target_type + } + ) + end + + post '/remote' do + redirect remote_confirm_uri + end + + private + + def remote_confirm_uri + URI::HTTPS.build( + host: Handle.parse(params[:handle]).domain, + path: '/remote_confirmation', + query: URI.encode_www_form( + action: action, + target: target, + target_type: target_type + ) + ) + end + + def action + 'follow' + end + + def message + 'Provide your handle to follow @luna@ravenclaw.example.org' + end + + def target + '@luna.ravenclaw.example.org' + end + + def target_type + 'account' + end + end +end diff --git a/app/web/policies/contact_policy.rb b/app/web/policies/contact_policy.rb index ca22493..07b3b3c 100644 --- a/app/web/policies/contact_policy.rb +++ b/app/web/policies/contact_policy.rb @@ -10,7 +10,7 @@ def add? private def anon? - actor.null? + actor.null? || aggregate.nil? || aggregate.null? end def self? diff --git a/app/web/views/_handle_field.erb b/app/web/views/_handle_field.erb new file mode 100644 index 0000000..fec8a26 --- /dev/null +++ b/app/web/views/_handle_field.erb @@ -0,0 +1,16 @@ + +
+ + @ + +
+ + +