diff --git a/.env.example b/.env.example
index be66839..e9342a8 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,13 @@
TOPSEKRIT_AUTHORISATION_SETTING='open'
TOPSEKRIT_AUTHORISED_DOMAIN='reinteractive.net'
GOOGLE_OAUTH_CALLBACK_PATH='/auth/google_oauth2/callback'
-GOOGLE_CLIENT_ID='
- Hello <%= @email %>
-
- Here is the requested authentication token that will allow you to access <%= link_to("SecretLink.org", "https://secretlink.org") %>:
-
- <%= link_to auth_token_url(@token), auth_token_url(@token) %>
-
- Thank you for using <%= link_to("SecretLink.org", "https://secretlink.org") %>!
-
- If you didn't expect to receive this message, please contact <%= mail_to "info@secretlink.org" %>.
- Welcome <%= @email %>! You can confirm your account email through the link below: <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %> Hello <%= @email %>! We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>. We're contacting you to notify you that your email has been changed to <%= @resource.email %>. Hello <%= @resource.email %>! We're contacting you to notify you that your password has been changed. Hello <%= @resource.email %>! Someone has requested a link to change your password. You can do this through the link below. <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %> If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one. Hello <%= @resource.email %>! Your account has been locked due to an excessive number of unsuccessful sign in attempts. Click the link below to unlock your account: <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %> Currently waiting confirmation for: <%= resource.unconfirmed_email %> Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
- If you want, you can just go ahead and create another secret with this address."
- end
- end
-
- def create
- auth_token = AuthTokenService.generate(auth_token_params)
- if auth_token.valid?
- flash.now[:message] = "A token has been generated and sent to #{auth_token_params['email']}"
- else
- # TODO: Remove the HTML formatting and let the view handle it.
- flash.now[:message] = auth_token.errors.full_messages.join("
".html_safe)
- end
- render :new
- end
-
- private
-
- def auth_token_params
- params.require(:auth_token).permit(:email)
- end
-
- def check_recaptcha
- unless verify_recaptcha
- flash[:error] = flash[:recaptcha_error]
- render :new
- end
- end
-end
diff --git a/app/controllers/authenticated_controller.rb b/app/controllers/authenticated_controller.rb
new file mode 100644
index 0000000..f9ed256
--- /dev/null
+++ b/app/controllers/authenticated_controller.rb
@@ -0,0 +1,3 @@
+class AuthenticatedController < ApplicationController
+ before_action :authenticate_user!
+end
diff --git a/app/controllers/concerns/retrieve_secret.rb b/app/controllers/concerns/retrieve_secret.rb
index a1f1e10..f1587fe 100644
--- a/app/controllers/concerns/retrieve_secret.rb
+++ b/app/controllers/concerns/retrieve_secret.rb
@@ -6,14 +6,14 @@ def retrieve_secret
@secret = Secret.find_by(uuid: params[:id])
case
when @secret.expired?
- flash[:error] = "Sorry, that secret has expired, please ask the person who sent it to you to send it again."
- redirect_to(new_auth_token_path)
+ flash[:error] = t('secrets.expired_error', from_email: @secret.from_email)
+ redirect_to(root_path)
when @secret.present? && SecretService.correct_key?(@secret, params[:key])
@secret
else
flash[:error] = "Sorry, we couldn't find that secret"
- redirect_to(new_auth_token_path)
+ redirect_to(root_path)
end
end
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
new file mode 100644
index 0000000..f206f1f
--- /dev/null
+++ b/app/controllers/dashboard_controller.rb
@@ -0,0 +1,10 @@
+class DashboardController < AuthenticatedController
+ def index
+ @secrets = current_user.secrets.order(created_at: :desc).first(10)
+
+ if @secrets.empty?
+ flash[:notice] = t('welcome')
+ redirect_to new_secret_path
+ end
+ end
+end
diff --git a/app/controllers/decrypted_secrets_controller.rb b/app/controllers/decrypted_secrets_controller.rb
index d462886..5cb1af2 100644
--- a/app/controllers/decrypted_secrets_controller.rb
+++ b/app/controllers/decrypted_secrets_controller.rb
@@ -1,7 +1,6 @@
class DecryptedSecretsController < ApplicationController
include RetrieveSecret
before_filter :retrieve_secret
- before_filter :require_validated_email
def create
begin
diff --git a/app/controllers/email_template_controller.rb b/app/controllers/email_template_controller.rb
new file mode 100644
index 0000000..52fbb7b
--- /dev/null
+++ b/app/controllers/email_template_controller.rb
@@ -0,0 +1,29 @@
+class EmailTemplateController < AuthenticatedController
+ layout 'settings'
+
+ def edit
+ @settings = current_user.settings
+
+ unless @settings.send_secret_email_template.present?
+ @settings.send_secret_email_template = build_default_email
+ end
+ end
+
+ def update
+ current_user.settings.update!(settings_params) # This step should never fail
+ redirect_to edit_email_template_path, notice: t('.success')
+ end
+
+ def build_default_email
+ @secret = Secret.new(from_email: current_user.email)
+
+ ViewBuilder.new(
+ UserSetting::DEFAULT_SEND_SECRET_EMAIL_TEMPLATE_PATH,
+ view_context.__binding__
+ ).run
+ end
+
+ def settings_params
+ params.require(:user_setting).permit(:send_secret_email_template)
+ end
+end
diff --git a/app/controllers/extended_secrets_controller.rb b/app/controllers/extended_secrets_controller.rb
new file mode 100644
index 0000000..a7fae31
--- /dev/null
+++ b/app/controllers/extended_secrets_controller.rb
@@ -0,0 +1,13 @@
+class ExtendedSecretsController < AuthenticatedController
+ def update
+ @secret = current_user.secrets.find(params[:id])
+
+ if @secret.expired? && !@secret.extended?
+ @secret.extend_expiry!
+ redirect_to dashboard_path, notice: t('secrets.extended_expiry', title: @secret.title)
+ else
+ # This case should not happen base on UI
+ redirect_to dashboard_path
+ end
+ end
+end
diff --git a/app/controllers/oauth_callbacks_controller.rb b/app/controllers/oauth_callbacks_controller.rb
index c360865..11c7498 100644
--- a/app/controllers/oauth_callbacks_controller.rb
+++ b/app/controllers/oauth_callbacks_controller.rb
@@ -3,19 +3,66 @@ class OauthCallbacksController < ApplicationController
def google
email = request.env['omniauth.auth'] && request.env['omniauth.auth']['info'] &&
request.env['omniauth.auth']['info']['email']
- if email
- flash[:message] = "Authenticated as \"#{email}\" via google"
- validate_email!(email)
- redirect_to new_secret_path
+
+ user = User.find_or_initialize_by(email: email)
+ if user.persisted?
+ handle_persisted_user(user)
else
- flash[:error] = 'Authentication via google failed'
- redirect_to new_auth_token_path
+ handle_new_user(user)
end
end
def auth_failure
- flash[:error] = 'Authentication failed'
- redirect_to new_auth_token_path
+ flash[:error] = t('oauth.failed')
+ redirect_to root_path
+ end
+
+ private
+
+ def handle_persisted_user(user)
+ if !user.confirmed?
+ redirect_to user_confirmation_path(confirmation_token: user.confirmation_token)
+ elsif user.confirmed? && user.encrypted_password.blank?
+ token = update_password_token(user)
+ redirect_to edit_user_setup_url(reset_password_token: token)
+ else
+ flash[:error] = t('oauth.already_registered')
+ redirect_to new_user_session_path
+ end
+ end
+
+ def handle_new_user(user)
+ user.skip_confirmation_notification!
+ if user.save
+ redirect_to user_confirmation_path(confirmation_token: user.confirmation_token)
+ else
+ handle_unauthorised and return if user.errors.added?(:email, t('field_errors.unauthorised'))
+ # This may not be necessary because a failed oauth calls directly
+ # to auth_failure, but keeping this here as a safeguard
+ auth_failure and return if user.errors.added?(:email, :blank)
+ handle_unknown_error(user)
+ end
+ end
+
+ def handle_unknown_error(user)
+ # We're intentionally raising an error here
+ # So tests will catch when creation of user with email only fails
+ raise user.errors.full_messages.to_s
+ end
+
+ def handle_unauthorised
+ flash[:error] = "Email #{t('field_errors.unauthorised')}"
+ redirect_to root_path
+ end
+
+ def update_password_token(user)
+ raw, enc = Devise.token_generator.generate(User, :reset_password_token)
+
+ user.reset_password_token = enc
+ user.reset_password_sent_at = Time.current.utc
+ user.save(validate: false)
+ user.reload
+ raw
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb
index e319e07..eb33e63 100644
--- a/app/controllers/pages_controller.rb
+++ b/app/controllers/pages_controller.rb
@@ -1,4 +1,7 @@
class PagesController < ApplicationController
+ def home
+ @user = User.new
+ end
def copyright
end
@@ -8,5 +11,4 @@ def privacy_policy
def terms_and_conditions
end
-
-end
\ No newline at end of file
+end
diff --git a/app/controllers/secrets_controller.rb b/app/controllers/secrets_controller.rb
index 91fe1b3..19a3a1c 100644
--- a/app/controllers/secrets_controller.rb
+++ b/app/controllers/secrets_controller.rb
@@ -1,37 +1,58 @@
-class SecretsController < ApplicationController
+class SecretsController < AuthenticatedController
include RetrieveSecret
before_filter :retrieve_secret, only: :show
- before_filter :require_validated_email, only: [:new, :create]
+ before_action :authenticate_user!, except: [:show]
def show
- # As the receipient has now clicked a link, we know their email address is also
- # valid, so we will validate them so they can painlessly send a new secret if
- # they like as well.
- validate_email!(@secret.to_email)
+ # TODO: Should we add a button to encourage the user to register?
end
def new
- @secret = Secret.new(from_email: validated_email)
+ base_secret = current_user.secrets.find_by(uuid: params[:base_id])
+
+ if base_secret.present?
+ @secret = Secret.new(
+ title: base_secret.title,
+ from_email: base_secret.from_email,
+ to_email: base_secret.to_email,
+ comments: base_secret.comments
+ )
+ else
+ @secret = Secret.new(from_email: current_user.email)
+ end
end
def create
- @secret = SecretService.encrypt_new_secret(secret_params)
+ @secret = SecretService.encrypt_new_secret(secret_params,
+ current_user.settings.send_secret_email_template)
+
if @secret.persisted?
- flash[:message] = "The secret has been encrypted and an email sent to the recipient, feel free to send another secret!"
- redirect_to new_secret_path
+ if @secret.no_email?
+ CopySecretService.new(session).prepare!(@secret)
+
+ flash[:message] = t('secrets.create.success.without_email')
+ redirect_to copy_secrets_path
+ else
+ flash[:message] = t('secrets.create.success.with_email')
+ redirect_to dashboard_path
+ end
else
- flash.now[:message] = @secret.errors.full_messages.join("
".html_safe)
+ flash.now[:error] = @secret.errors.full_messages.join("
".html_safe)
render :new
end
end
+ def copy
+ @data = CopySecretService.new(session).perform!
+ redirect_to root_path unless @data
+ end
+
private
def secret_params
params.require(:secret).permit(:title, :to_email, :secret, :comments,
- :expire_at, :secret_file).tap do |p|
- p[:from_email] = validated_email
+ :expire_at, :secret_file, :no_email).tap do |p|
+ p[:from_email] = current_user.email
end
end
-
end
diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb
new file mode 100644
index 0000000..3bcef07
--- /dev/null
+++ b/app/controllers/subscriptions_controller.rb
@@ -0,0 +1,11 @@
+class SubscriptionsController < AuthenticatedController
+ layout 'settings'
+
+ def new
+ end
+
+ def create
+ SubscriptionService.new(current_user)
+ .perform(params[:stripe_source])
+ end
+end
diff --git a/app/controllers/two_factor_auth_controller.rb b/app/controllers/two_factor_auth_controller.rb
new file mode 100644
index 0000000..f20249e
--- /dev/null
+++ b/app/controllers/two_factor_auth_controller.rb
@@ -0,0 +1,44 @@
+class TwoFactorAuthController < AuthenticatedController
+ layout 'settings'
+
+ def edit
+ @tfa_service = TwoFactorService.new(current_user)
+ @tfa_service.issue_otp_secret
+ @otp_provisioning_uri = @tfa_service.generate_otp_provisioning_uri
+ end
+
+ def update
+ @tfa_service = TwoFactorService.new(current_user)
+
+ valid =
+ if enabling_otp?
+ @tfa_service.enable_otp(
+ two_factor_params[:otp_secret],
+ two_factor_params[:otp_attempt],
+ two_factor_params[:current_password]
+ )
+ else
+ @tfa_service.disable_otp(two_factor_params[:current_password])
+ end
+
+ if valid
+ verb = @tfa_service.user.otp_required_for_login ? 'enabled' : 'disabled'
+ redirect_to root_path, notice: t('two_factor.update_success', verb: verb)
+ else
+ flash[:error] = t('two_factor.update_failed')
+
+ @otp_provisioning_uri = @tfa_service.generate_otp_provisioning_uri
+ render :edit
+ end
+ end
+
+ private
+
+ def enabling_otp?
+ two_factor_params[:otp_required_for_login] == '1'
+ end
+
+ def two_factor_params
+ params.require(:user).permit(:otp_required_for_login, :otp_secret, :otp_attempt, :current_password)
+ end
+end
diff --git a/app/controllers/user_setups_controller.rb b/app/controllers/user_setups_controller.rb
new file mode 100644
index 0000000..e7dbedf
--- /dev/null
+++ b/app/controllers/user_setups_controller.rb
@@ -0,0 +1,51 @@
+class UserSetupsController < ApplicationController
+ before_action :set_original_token
+
+ def edit
+ @user = User.with_reset_password_token(@original_token)
+
+ if @user
+ @tfa_service = TwoFactorService.new(@user)
+ @tfa_service.issue_otp_secret
+ @otp_provisioning_uri = @tfa_service.generate_otp_provisioning_uri
+ else
+ redirect_to root_path, notice: t('devise.passwords.no_token')
+ end
+ end
+
+ def update
+ @setup_service = UserSetupService.new(
+ password_params[:reset_password_token],
+ TwoFactorService
+ )
+
+ if @setup_service.run(password_params, two_factor_params)
+ @setup_service.user.after_database_authentication
+ flash[:notice] = t('devise.passwords.updated')
+ sign_in(@setup_service.user)
+
+ redirect_to dashboard_path
+ else
+ @tfa_service = @setup_service.tfa_service
+ @tfa_service.user.otp_required_for_login = two_factor_params[:otp_required_for_login]
+ @tfa_service.user.otp_secret = two_factor_params[:otp_secret]
+ @otp_provisioning_uri = @tfa_service.generate_otp_provisioning_uri
+
+ render :edit
+ end
+ end
+
+ private
+
+ def password_params
+ params.require(:user).permit(:password, :password_confirmation, :reset_password_token)
+ end
+
+ def two_factor_params
+ params.require(:user).permit(:otp_required_for_login, :otp_secret, :otp_attempt)
+ end
+
+ def set_original_token
+ @original_token ||= params[:reset_password_token]
+ end
+end
diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb
new file mode 100644
index 0000000..0353df8
--- /dev/null
+++ b/app/controllers/users/confirmations_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Users::ConfirmationsController < Devise::ConfirmationsController
+ # GET /resource/confirmation/new
+ # def new
+ # super
+ # end
+
+ # POST /resource/confirmation
+ # def create
+ # super
+ # end
+
+ # GET /resource/confirmation?confirmation_token=abcdef
+ # def show
+ # super
+ # end
+
+ # protected
+
+ # The path used after resending confirmation instructions.
+ # def after_resending_confirmation_instructions_path_for(resource_name)
+ # super(resource_name)
+ # end
+
+ # The path used after confirmation.
+ def after_confirmation_path_for(resource_name, resource)
+ token = resource.send(:set_reset_password_token)
+ edit_user_setup_url(reset_password_token: token)
+ end
+end
diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb
new file mode 100644
index 0000000..093ac71
--- /dev/null
+++ b/app/controllers/users/passwords_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class Users::PasswordsController < Devise::PasswordsController
+ # GET /resource/password/new
+ # def new
+ # super
+ # end
+
+ # POST /resource/password
+ # def create
+ # super
+ # end
+
+ # GET /resource/password/edit?reset_password_token=abcdef
+ # def edit
+ # super
+ # end
+
+ # PUT /resource/password
+ # def update
+ # super
+ # end
+
+ # protected
+
+ def after_resetting_password_path_for(resource)
+ # TODO: this path is also being used by user registration flow
+ # (After confirming the account, the user is asked to edit/set the password)
+ # Please fix accordingly if the path is not desired
+ # when editing password out of registration's context
+ dashboard_path
+ end
+
+ # The path used after sending reset password instructions
+ # def after_sending_reset_password_instructions_path_for(resource_name)
+ # super(resource_name)
+ # end
+end
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
new file mode 100644
index 0000000..e195e2e
--- /dev/null
+++ b/app/controllers/users/registrations_controller.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+class Users::RegistrationsController < Devise::RegistrationsController
+ before_action :check_recaptcha, only: :create
+ # before_action :configure_sign_up_params, only: [:create]
+ # before_action :configure_account_update_params, only: [:update]
+
+ # GET /resource/sign_up
+ # def new
+ # super
+ # end
+
+ # POST /resource
+ def create
+ user = User.find_by(email: params[:user][:email])
+
+ if user.present? && !user.confirmed?
+ user.send_confirmation_instructions
+ redirect_to root_path, notice: t('devise.registrations.signed_up_but_unconfirmed')
+ elsif user.present? && user.confirmed? && user.encrypted_password.blank?
+ user.send_reset_password_instructions
+ redirect_to root_path, notice: t('devise.registrations.signed_up_confirmed_without_password')
+ else
+ super
+ end
+ end
+
+ # GET /resource/edit
+ # def edit
+ # super
+ # end
+
+ # PUT /resource
+ # def update
+ # super
+ # end
+
+ # DELETE /resource
+ # def destroy
+ # super
+ # end
+
+ # GET /resource/cancel
+ # Forces the session data which is usually expired after sign
+ # in to be expired now. This is useful if the user wants to
+ # cancel oauth signing in/up in the middle of the process,
+ # removing all OAuth session data.
+ # def cancel
+ # super
+ # end
+
+ # protected
+
+ # If you have extra params to permit, append them to the sanitizer.
+ # def configure_sign_up_params
+ # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
+ # end
+
+ # If you have extra params to permit, append them to the sanitizer.
+ # def configure_account_update_params
+ # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
+ # end
+
+ # The path used after sign up.
+ # def after_sign_up_path_for(resource)
+ # super(resource)
+ # end
+
+ # The path used after sign up for inactive accounts.
+ # def after_inactive_sign_up_path_for(resource)
+ # super(resource)
+ # end
+
+ private
+
+ def check_recaptcha
+ unless verify_recaptcha
+ self.resource = resource_class.new sign_up_params
+ resource.validate # Look for any other validation errors besides Recaptcha
+ resource.errors.add(:base, 'reCAPTCHA verification failed, please try again')
+ respond_with resource
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index caccb42..fd74c1d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -9,7 +9,7 @@ def to_email_placeholder
end
def from_email_placeholder
- validated_email
+ current_user.email
end
# A closed system only allows secrets to be sent to and from email
@@ -32,4 +32,7 @@ def auth_new_page?
request.path == '/auth_tokens/new'
end
+ def flash_display_type(key)
+ HashWithIndifferentAccess.new(alert: 'danger', error: 'danger')[key] || 'success'
+ end
end
diff --git a/app/mailers/secret_mailer.rb b/app/mailers/secret_mailer.rb
index 420a4e8..01cd868 100644
--- a/app/mailers/secret_mailer.rb
+++ b/app/mailers/secret_mailer.rb
@@ -1,7 +1,11 @@
class SecretMailer < BaseMailer
- def secret_notification(secret)
+ def secret_notification(secret, custom_message = nil)
@secret = secret
+
+ #We're also checking for blank string
+ @editable_content = custom_message.present? ? custom_message : load_default_content
+
mail(to: @secret.to_email,
reply_to: @secret.from_email,
subject: "SecretLink.org: A secret has been shared with you - Reference #{@secret.uuid}")
@@ -14,4 +18,12 @@ def consumnation_notification(secret)
subject: "Your secret was consumed on SecretLink.org - Reference #{@secret.uuid}")
end
+ private
+
+ def load_default_content
+ ViewBuilder.new(
+ UserSetting::DEFAULT_SEND_SECRET_EMAIL_TEMPLATE_PATH,
+ view_context.__binding__
+ ).run
+ end
end
diff --git a/app/models/auth_token.rb b/app/models/auth_token.rb
index a339305..dfd0939 100644
--- a/app/models/auth_token.rb
+++ b/app/models/auth_token.rb
@@ -4,17 +4,11 @@ class AuthToken < ActiveRecord::Base
before_validation :set_defaults
- # TODO: Model shouldn't be sending the email.
- # TODO: Emails should be in background worker.
- def notify
- AuthTokenMailer.auth_token(email, hashed_token).deliver_now
- end
-
private
def set_defaults
self.hashed_token = SecureRandom.hex
- self.expire_at = Time.now + 7.days
+ self.expire_at = Time.current + 7.days
end
def email_domain_authorised
diff --git a/app/models/secret.rb b/app/models/secret.rb
index c84d9de..8711943 100644
--- a/app/models/secret.rb
+++ b/app/models/secret.rb
@@ -9,8 +9,8 @@ class Secret < ActiveRecord::Base
}
validates :to_email, presence: {
- message: "Please enter the senders's email address"
- }
+ message: "Please enter the senders's email address"
+ }, unless: :no_email
validates :secret, presence: {
message: "Please enter a secret to share with the recipient",
@@ -23,24 +23,43 @@ class Secret < ActiveRecord::Base
where('from_email = ? and access_key = ?', email, access_key)
}
+ belongs_to :user, primary_key: 'email', foreign_key: 'from_email'
+
+ def sent_at
+ # We're using creted_at as the send date
+ created_at
+ end
+
def delete_encrypted_information
update_attribute(:secret, nil)
end
def mark_as_consumed
- update_attribute(:consumed_at, Time.now)
+ update_attribute(:consumed_at, Time.current)
end
def expired?
- expire_at.present? && expire_at < Time.now
+ expire_at.present? && expire_at < Time.current
+ end
+
+ def extend_expiry!
+ # We need to use update_columns to bypass reencryption
+ update_columns(
+ expire_at: Time.current + 1.week,
+ extended_at: Time.current
+ )
+ end
+
+ def extended?
+ extended_at.present?
end
def expire_at_within_limit
if Rails.application.config.topsekrit_maximum_expiry_time
- max_expiry_in_config = (Time.now + Rails.application.config.topsekrit_maximum_expiry_time).to_i
+ max_expiry_in_config = (Time.current + Rails.application.config.topsekrit_maximum_expiry_time).to_i
if expire_at.blank? || (expire_at && expire_at.to_i > max_expiry_in_config)
errors.add(:expire_at, "Maximum expiry allowed is " +
- (Time.now + Rails.application.config.topsekrit_maximum_expiry_time).strftime('%d %B %Y'))
+ (Time.current + Rails.application.config.topsekrit_maximum_expiry_time).strftime('%d %B %Y'))
end
end
end
@@ -66,7 +85,7 @@ def email_domain_does_not_match?
def self.expire_at_hint
if Rails.application.config.topsekrit_maximum_expiry_time
(Date.today + 1).strftime('%d %B %Y') + ' - ' +
- (Time.now + Rails.application.config.topsekrit_maximum_expiry_time).strftime('%d %B %Y')
+ (Time.current + Rails.application.config.topsekrit_maximum_expiry_time).strftime('%d %B %Y')
end
end
end
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
new file mode 100644
index 0000000..bba8816
--- /dev/null
+++ b/app/models/subscription.rb
@@ -0,0 +1,5 @@
+class Subscription < ActiveRecord::Base
+ PLANS = YAML.load_file(Rails.root.join("db", "data", "subscription_plans.yml"))
+
+ enum status: [:active, :inactive]
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..e198c45
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,38 @@
+class User < ActiveRecord::Base
+ # Include default devise modules. Others available are:
+ # :lockable, :timeoutable and :omniauthable
+ after_create :create_settings
+
+ devise :registerable, :confirmable,
+ :recoverable, :rememberable, :trackable, :validatable
+
+ devise :two_factor_authenticatable,
+ otp_secret_encryption_key: Rails.configuration.topsekrit_2fa_key
+
+ validate :email_authorised?, on: :create
+
+ has_one :settings, class_name: 'UserSetting'
+ has_many :secrets, primary_key: 'email', foreign_key: 'from_email'
+
+ protected
+
+ def password_required?
+ confirmed? ? super : false
+ end
+
+ def email_authorised?
+ unless AuthorisedEmailService.authorised_to_register?(email)
+ errors.add(:email, I18n.t('field_errors.unauthorised'))
+ end
+ end
+
+ def self.with_reset_password_token(token)
+ reset_password_token = Devise.token_generator.digest(self, :reset_password_token, token)
+ to_adapter.find_first(reset_password_token: reset_password_token)
+ end
+
+ # hooks
+ def create_settings
+ UserSetting.create(user: self)
+ end
+end
diff --git a/app/models/user_setting.rb b/app/models/user_setting.rb
new file mode 100644
index 0000000..c888836
--- /dev/null
+++ b/app/models/user_setting.rb
@@ -0,0 +1,6 @@
+class UserSetting < ActiveRecord::Base
+ DEFAULT_SEND_SECRET_EMAIL_TEMPLATE_PATH =
+ 'secret_mailer/secret_notification_editable.html.erb'.freeze
+
+ belongs_to :user
+end
diff --git a/app/services/auth_token_service.rb b/app/services/auth_token_service.rb
deleted file mode 100644
index e0980d8..0000000
--- a/app/services/auth_token_service.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class AuthTokenService
-
- def self.generate(auth_hash)
- auth_token = AuthToken.create(auth_hash)
- # TODO: Model shouldn't be sending the email
- auth_token.notify if auth_token.persisted?
- auth_token
- end
-
-end
diff --git a/app/services/authorised_email_service.rb b/app/services/authorised_email_service.rb
index 9d9328b..104b66e 100644
--- a/app/services/authorised_email_service.rb
+++ b/app/services/authorised_email_service.rb
@@ -1,12 +1,45 @@
-class AuthorisedEmailService
+module AuthorisedEmailService
+ class << self
+ def closed_system?
+ authorisation == :closed
+ end
- def self.email_domain_matches?(email)
- regexp = Regexp.new(".+#{Rails.configuration.topsekrit_authorised_domain}\\z")
- email.to_s.match(regexp)
- end
+ def limited_system?
+ authorisation == :limited
+ end
+
+ def open_system?
+ authorisation == :open
+ end
+
+ def closed_or_limited_system?
+ closed_system? || limited_system?
+ end
+
+ def authorised_to_register?(email)
+ if closed_or_limited_system?
+ email_domain_matches?(email).present?
+ else
+ true
+ end
+ end
+
+ def email_domain_matches?(email)
+ regexp = Regexp.new(".+#{authorised_domain}\\z")
+ email.to_s.match(regexp)
+ end
+
+ def email_domain_does_not_match?(email)
+ !email_domain_matches?(email)
+ end
+
+ def authorisation
+ Rails.configuration.topsekrit_authorisation_setting
+ end
- def self.email_domain_does_not_match?(email)
- !email_domain_matches?(email)
+ def authorised_domain
+ Rails.configuration.topsekrit_authorised_domain
+ end
end
-end
\ No newline at end of file
+end
diff --git a/app/services/copy_secret_service.rb b/app/services/copy_secret_service.rb
new file mode 100644
index 0000000..9947940
--- /dev/null
+++ b/app/services/copy_secret_service.rb
@@ -0,0 +1,27 @@
+class CopySecretService
+ attr_reader :session
+
+ KEY = :copy_secret_key
+ UUID = :copy_secret_uuid
+
+ def initialize(session)
+ @session = session
+ end
+
+ def prepare!(secret)
+ session[KEY] = secret.secret_key
+ session[UUID] = secret.uuid
+ end
+
+ def perform!
+ if session[KEY] && session[UUID]
+ data = {key: session[KEY], uuid: session[UUID]}
+
+ session.delete(KEY)
+ session.delete(UUID)
+ data
+ else
+ false
+ end
+ end
+end
diff --git a/app/services/secret_service.rb b/app/services/secret_service.rb
index e345efc..61441d9 100644
--- a/app/services/secret_service.rb
+++ b/app/services/secret_service.rb
@@ -1,10 +1,13 @@
class SecretService
- def self.encrypt_new_secret(params)
+ def self.encrypt_new_secret(params, email_template = nil)
secret = Secret.create(params.merge(uuid: SecureRandom.uuid, secret_key: SecureRandom.hex(16)))
- if secret.persisted?
+ if secret.persisted? && !secret.no_email?
# TODO: Mailers should be in the background
- SecretMailer.secret_notification(secret).deliver_now
+ SecretMailer.secret_notification(
+ secret,
+ email_template
+ ).deliver_now
end
secret
end
diff --git a/app/services/subscription_service.rb b/app/services/subscription_service.rb
new file mode 100644
index 0000000..1c4c5ec
--- /dev/null
+++ b/app/services/subscription_service.rb
@@ -0,0 +1,54 @@
+require 'ostruct'
+
+class SubscriptionService
+ attr_reader :user, :plan
+
+ def initialize(user)
+ @user = user
+
+ # Right now we're just supporting 1 type of plan
+ @plan = OpenStruct.new(Subscription::PLANS['default_monthly'])
+ end
+
+ def perform(source_id)
+ customer = create_customer(source_id)
+
+ # TODO: Use same customer id when present
+ # Or Persist if customer is new
+ result = subscribe_to_plan(customer["id"])
+
+ # TODO:
+ # Persist subscription
+ build_subscription(result)
+ end
+
+ private
+
+ def create_customer(source_id)
+ Stripe::Customer.create({
+ email: user.email,
+ source: source_id
+ })
+ end
+
+ def subscribe_to_plan(customer_id)
+ Stripe::Subscription.create({
+ customer: customer_id,
+ items: [{
+ plan: plan.stripe_id
+ }],
+ })
+ end
+
+ def build_subscription(stripe_subscription)
+ # TODO: Handle failure
+ if stripe_subscription.status == "active"
+ Subscription.create!(
+ code: plan.code,
+ status: :active,
+ cached_metadata: plan.to_h,
+ cached_transaction_details: stripe_subscription.to_json
+ )
+ end
+ end
+end
diff --git a/app/services/two_factor_service.rb b/app/services/two_factor_service.rb
new file mode 100644
index 0000000..372c934
--- /dev/null
+++ b/app/services/two_factor_service.rb
@@ -0,0 +1,56 @@
+class TwoFactorService
+ attr_reader :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def issue_otp_secret
+ secret = User.generate_otp_secret
+ user.otp_secret = secret
+ secret
+ end
+
+ def generate_otp_provisioning_uri
+ issuer = 'Secret Link'
+ issuer += ' (dev)' if Rails.env.development?
+ label = "#{issuer}:#{user.email}"
+ user.otp_provisioning_uri(label, issuer: issuer)
+ end
+
+ # Update model with params only if otp_attempt and current_password are correct
+ def enable_otp(otp_secret, otp_attempt, current_password)
+ user.assign_attributes(otp_secret: otp_secret, otp_required_for_login: true)
+
+ result =
+ if user.valid_password?(current_password)
+ validate_and_consume_otp(otp_attempt, otp_secret)
+ else
+ user.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
+ false
+ end
+
+ user.clean_up_passwords
+ result
+ end
+
+ def enable_otp_without_password(otp_secret, otp_attempt)
+ validate_and_consume_otp(otp_attempt, otp_secret)
+ end
+
+ def disable_otp(current_password)
+ user.update_with_password(current_password: current_password, otp_required_for_login: false)
+ end
+
+ private
+
+ def validate_and_consume_otp(otp_attempt, otp_secret)
+ if user.validate_and_consume_otp!(otp_attempt, otp_secret: otp_secret)
+ user.update_attributes(otp_secret: otp_secret, otp_required_for_login: true)
+ true
+ else
+ user.errors.add(:otp_attempt, otp_attempt.blank? ? :blank : :invalid)
+ false
+ end
+ end
+end
diff --git a/app/services/user_setup_service.rb b/app/services/user_setup_service.rb
new file mode 100644
index 0000000..d6d4d52
--- /dev/null
+++ b/app/services/user_setup_service.rb
@@ -0,0 +1,46 @@
+class UserSetupService
+ attr_reader :user, :tfa_service
+
+ def initialize(token, tfa_service_klass)
+ reset_password_token = Devise.token_generator.digest(User, :reset_password_token, token)
+
+ @user = User.find_or_initialize_with_error_by(:reset_password_token, reset_password_token)
+ @tfa_service = tfa_service_klass.new(@user)
+ end
+
+ def run(password_params, tfa_params)
+ password, confirmation = password_params.values_at :password, :password_confirmation
+ otp_required, otp_secret, otp_attempt = tfa_params.values_at :otp_required_for_login, :otp_secret, :otp_attempt
+
+ if token_valid? && user_valid?(password, confirmation)
+ return enable_otp(otp_secret, otp_attempt) if otp_required == '1'
+ user.save! # This should never raise an errror
+ else
+ false
+ end
+ end
+
+ private
+
+ def enable_otp(secret, attempt)
+ if tfa_service.enable_otp_without_password(secret, attempt)
+ user.save! # This should never raise an errror
+ else
+ false
+ end
+ end
+
+ def user_valid?(password, confirmation)
+ if password.present?
+ user.assign_attributes(password: password, password_confirmation: confirmation)
+ user.valid?
+ else
+ user.errors.add(:password, :blank)
+ false
+ end
+ end
+
+ def token_valid?
+ user.persisted? && user.reset_password_period_valid?
+ end
+end
diff --git a/app/views/auth_token_mailer/auth_token.html.erb b/app/views/auth_token_mailer/auth_token.html.erb
deleted file mode 100644
index 74a44a6..0000000
--- a/app/views/auth_token_mailer/auth_token.html.erb
+++ /dev/null
@@ -1,21 +0,0 @@
-
-All Secrets
+ <%= link_to 'Create New', new_secret_path, class: 'button green' %>
+ Resend confirmation instructions
+
+ Set your password
+
+ Forgot your password?
+
+ Edit <%= resource_name.to_s.humanize %>
+
+<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
+ <%= f.error_notification %>
+
+ Cancel my account
+
+Sign up
+
+ Log in
+
+
+<% end -%>
+
+<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
+ <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
+ <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end -%>
+
+<%- if devise_mapping.omniauthable? %>
+ <%- resource_class.omniauth_providers.each do |provider| %>
+ <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
+ <% end -%>
+<% end -%>
diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb
new file mode 100644
index 0000000..788f62e
--- /dev/null
+++ b/app/views/devise/unlocks/new.html.erb
@@ -0,0 +1,16 @@
+Resend unlock instructions
+
+<%= simple_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
+ <%= f.error_notification %>
+ <%= f.full_error :unlock_token %>
+
+ Email Template
+
+ Preview:
+ Share secrets such as passwords, API keys, and SSL certificates simply and securely.
- <%= simple_form_for :auth_token, url: auth_tokens_url, html: { class: '' } do |f| %>
- Share a secret now...
- <%= f.input :email, label: false, placeholder: 'To send a secret, enter your email address...', input_html: { class: 'form-control center-block' } %>
- <%= invisible_recaptcha_tags text: 'Send SecretLink.org Token', class: 'button white' %>
- <%= link_to "Send using Google", '/auth/google_oauth2', id: 'oauth-google', class: "google-link" %>
- <% end %>
+ <% unless user_signed_in? %>
+ <%= simple_form_for(@user, url: registration_path(@user)) do |f| %>
+ Share a secret now...
+ <%= f.input :email, required: true, label: false, placeholder: 'Enter your email address', input_html: { class: 'form-control center-block' } %>
+ <%= invisible_recaptcha_tags text: 'Register', class: 'button white' %>
+
+ <%= link_to "Register with a Google Account", '/auth/google_oauth2', id: 'oauth-google', class: "google-link" %>
+ <% end %>
+ <% end %>
- <%= @secret.from_email %> has shared a secret with you via <%= link_to("SecretLink.org", "https://SecretLink.org") %>.
+ <%= sanitize @editable_content %>
Thank you for using <%= link_to("SecretLink.org", "https://SecretLink.org") %>!
- diff --git a/app/views/secret_mailer/secret_notification.text.erb b/app/views/secret_mailer/secret_notification.text.erb index e1dfe98..e0d38e0 100644 --- a/app/views/secret_mailer/secret_notification.text.erb +++ b/app/views/secret_mailer/secret_notification.text.erb @@ -1,7 +1,7 @@ Hello <%= @secret.to_email %> -<%= @secret.from_email %> has shared a secret with you via SecretLink.org. +<%= HTMLToTextParser.new(sanitize(@editable_content)).run %> <% if @secret.title.present? %> Title: <%= @secret.title %> diff --git a/app/views/secret_mailer/secret_notification_editable.html.erb b/app/views/secret_mailer/secret_notification_editable.html.erb new file mode 100644 index 0000000..ed58794 --- /dev/null +++ b/app/views/secret_mailer/secret_notification_editable.html.erb @@ -0,0 +1 @@ +<%= @secret.from_email %> has shared a secret with you via <%= link_to("SecretLink.org", "https://SecretLink.org") %>. diff --git a/app/views/secrets/copy.html.erb b/app/views/secrets/copy.html.erb new file mode 100644 index 0000000..8215ea1 --- /dev/null +++ b/app/views/secrets/copy.html.erb @@ -0,0 +1,11 @@ +<%= secret_url(@data[:uuid], key: @data[:key]) %>+ +