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='' -GOOGLE_CLIENT_SECRET='' +GOOGLE_OAUTH_CLIENT_ID='' +GOOGLE_OAUTH_CLIENT_SECRET='' BASE_URL='http://localhost:3000' -TOPSEKRIT_EXPIRY_DAYS='14' \ No newline at end of file +TOPSEKRIT_EXPIRY_DAYS='14' +RECAPTCHA_SITE_KEY='' +RECAPTCHA_SECRET_KEY='' +2FA_KEY='' +STRIPE_PUBLISHABLE_KEY='' +STRIPE_SECRET_KEY='' +STRIPE_SUBSCRIPTION_PLAN_ID='' diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 0000000..3691f8d --- /dev/null +++ b/Architecture.md @@ -0,0 +1,19 @@ +# Architecture +* Last update: July 25, 2019 +* This is a living document. Please update this whenever you can. +* This is not 100% complete, the intention is to give you an overview of the system + +## Models +### User +``` +has_many :subscriptions +``` + +### Subscription +``` +code: string +status: integer # enum + +cached_metadata: json +cached_transaction_details: json +``` diff --git a/Gemfile b/Gemfile index e26bcf1..dbb02d4 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,13 @@ gem "bugsnag" gem "okcomputer" gem "skylight" gem 'rails_12factor', group: :production +gem "devise" +gem "devise-two-factor" +gem "rqrcode-rails3" +gem "mini_magick" +gem "local_time" +gem "trix" +gem "stripe" gem 'sdoc', '~> 0.4.0', group: :doc @@ -41,14 +48,16 @@ group :test do gem "capybara" gem "poltergeist" gem "rspec-rails" - gem "factory_girl_rails" + gem "factory_bot_rails" gem "database_cleaner" gem "faker" gem "launchy" gem "show_me_the_cookies", "~> 3.1.0" + gem "timecop" end group :development, :test do gem "byebug" gem "dotenv-rails" + gem 'pry' end diff --git a/Gemfile.lock b/Gemfile.lock index d489b0d..13bdbf0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,6 +43,7 @@ GEM encryptor (~> 3.0.0) autoprefixer-rails (6.7.7.1) execjs + bcrypt (3.1.11) better_errors (2.1.1) coderay (>= 1.0.0) erubis (>= 2.6.6) @@ -66,11 +67,25 @@ GEM activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) + chunky_png (1.3.11) cliver (0.3.2) coderay (1.1.1) concurrent-ruby (1.0.5) + connection_pool (2.2.2) database_cleaner (1.5.3) debug_inspector (0.0.2) + devise (4.4.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0, < 5.2) + responders + warden (~> 1.2.3) + devise-two-factor (3.0.3) + activesupport (< 5.3) + attr_encrypted (>= 1.3, < 4, != 2) + devise (~> 4.0) + railties (< 5.3) + rotp (~> 2.0) diff-lcs (1.3) dotenv (2.2.0) dotenv-rails (2.2.0) @@ -81,14 +96,14 @@ GEM encryptor (3.0.0) erubis (2.7.0) execjs (2.7.0) - factory_girl (4.8.0) - activesupport (>= 3.0.0) - factory_girl_rails (4.8.0) - factory_girl (~> 4.8.0) - railties (>= 3.0.0) + factory_bot (5.0.2) + activesupport (>= 4.2.0) + factory_bot_rails (5.0.2) + factory_bot (~> 5.0.2) + railties (>= 4.2.0) faker (1.7.3) i18n (~> 0.5) - faraday (0.11.0) + faraday (0.15.4) multipart-post (>= 1.2, < 3) globalid (0.3.7) activesupport (>= 4.1.0) @@ -106,6 +121,7 @@ GEM kgio (2.11.0) launchy (2.4.3) addressable (~> 2.3) + local_time (2.1.0) logstash-event (1.2.02) logstasher (0.6.5) logstash-event (~> 1.2.0) @@ -114,19 +130,23 @@ GEM nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) + method_source (0.9.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) + mini_magick (4.9.3) mini_portile2 (2.1.0) minitest (5.10.1) - multi_json (1.12.1) + multi_json (1.13.1) multi_xml (0.6.0) - multipart-post (2.0.0) + multipart-post (2.1.1) + net-http-persistent (3.0.1) + connection_pool (~> 2.2) nokogiri (1.7.1) mini_portile2 (~> 2.1.0) - oauth2 (1.3.1) - faraday (>= 0.8, < 0.12) - jwt (~> 1.0) + oauth2 (1.4.1) + faraday (>= 0.8, < 0.16.0) + jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) @@ -142,6 +162,7 @@ GEM omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) + orm_adapter (0.5.0) parser (2.4.0.0) ast (~> 2.2) pg (0.20.0) @@ -152,10 +173,13 @@ GEM cliver (~> 0.3.1) websocket-driver (>= 0.2.0) powerpack (0.1.1) + pry (0.12.2) + coderay (~> 1.1.0) + method_source (~> 0.9.0) public_suffix (2.0.5) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) - rack (1.6.5) + rack (1.6.11) rack-test (0.6.3) rack (>= 1.0) rails (4.2.8) @@ -194,6 +218,14 @@ GEM recaptcha (4.10.0) json request_store (1.3.2) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) + rotp (2.1.2) + rqrcode (0.10.1) + chunky_png (~> 1.0) + rqrcode-rails3 (0.1.7) + rqrcode (>= 0.4.2) rspec-core (3.5.4) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) @@ -242,9 +274,15 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + stripe (4.21.2) + faraday (~> 0.13) + net-http-persistent (~> 3.0) thor (0.19.4) thread_safe (0.3.6) tilt (2.0.7) + timecop (0.9.1) + trix (0.11.1) + rails (> 4.1, < 5.2) tzinfo (1.2.3) thread_safe (~> 0.1) uglifier (3.2.0) @@ -256,6 +294,8 @@ GEM unicorn-rails (2.2.1) rack unicorn + warden (1.2.7) + rack (>= 1.0) web-console (2.3.0) activemodel (>= 4.0) binding_of_caller (>= 0.7.2) @@ -280,23 +320,29 @@ DEPENDENCIES capybara carrierwave database_cleaner + devise + devise-two-factor dotenv-rails email_validator - factory_girl_rails + factory_bot_rails faker jbuilder (~> 2.0) jquery-rails launchy + local_time logstasher (~> 0.6.5) + mini_magick okcomputer omniauth-google-oauth2 pg pickadate-rails poltergeist + pry quiet_assets rails (~> 4.2.8) rails_12factor recaptcha + rqrcode-rails3 rspec-rails rubocop sass-rails (~> 5.0) @@ -304,6 +350,9 @@ DEPENDENCIES show_me_the_cookies (~> 3.1.0) simple_form skylight + stripe + timecop + trix uglifier (>= 1.3.0) unicorn unicorn-rails diff --git a/README.md b/README.md index f592457..0ee136c 100644 --- a/README.md +++ b/README.md @@ -145,4 +145,3 @@ Some helpful commands: heroku restart -a rei-secretlink-production heroku logs --tail -a rei-secretlink-production - diff --git a/app/assets/images/apple-app-store.png b/app/assets/images/apple-app-store.png new file mode 100644 index 0000000..77a11d3 Binary files /dev/null and b/app/assets/images/apple-app-store.png differ diff --git a/app/assets/images/google-play-store.png b/app/assets/images/google-play-store.png new file mode 100644 index 0000000..21cc539 Binary files /dev/null and b/app/assets/images/google-play-store.png differ diff --git a/app/assets/images/locked-icon.svg b/app/assets/images/locked-icon.svg new file mode 100644 index 0000000..e55bf0d --- /dev/null +++ b/app/assets/images/locked-icon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/app/assets/images/unlocked-icon.svg b/app/assets/images/unlocked-icon.svg new file mode 100644 index 0000000..88d1343 --- /dev/null +++ b/app/assets/images/unlocked-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 1ac51c7..c37004e 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -10,8 +10,17 @@ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. //= require jquery +//= require jquery_ujs //= require pickadate/picker //= require pickadate/picker.date //= require bootstrap-sprockets //= require topsekrit //= require ui-functions +//= require local-time +//= require trix + +// Components +//= require components/secret-item +//= require components/two-factor-fields +//= require components/new-secret-form +//= require components/stripe-form diff --git a/app/assets/javascripts/components/new-secret-form.js b/app/assets/javascripts/components/new-secret-form.js new file mode 100644 index 0000000..4a7c209 --- /dev/null +++ b/app/assets/javascripts/components/new-secret-form.js @@ -0,0 +1,32 @@ +$(function() { + function NewSecretForm(rootDom) { + this.$root = $(rootDom); + this.$noEmailToggle = this.$root.find('.no-email-toggle'); + this.$fromEmailField = this.$root.find('.secret_from_email'); + this.$toEmailField = this.$root.find('.secret_to_email'); + this.$toEmailInput = this.$root.find("input[name='secret[to_email]']") + this.init(); + } + + NewSecretForm.prototype.init = function () { + this.$noEmailToggle.change(function() { + this.toggleEmailFields(!this.withoutEmail()); + }.bind(this)); + } + + NewSecretForm.prototype.toggleEmailFields = function (showField) { + this.$fromEmailField.toggle(showField); + this.$toEmailField.toggle(showField); + this.$toEmailInput.val(''); + } + + NewSecretForm.prototype.withoutEmail = function () { + return this.$noEmailToggle.is(':checked'); + } + + //Intialization code + const $newSecretForm = $(this).find(".js-new-secret-form"); + $newSecretForm.each(function () { + new NewSecretForm(this) + }) +}); diff --git a/app/assets/javascripts/components/secret-item.js b/app/assets/javascripts/components/secret-item.js new file mode 100644 index 0000000..d40069b --- /dev/null +++ b/app/assets/javascripts/components/secret-item.js @@ -0,0 +1,22 @@ +$(function() { + function SecretItem(rootDom) { + this.$root = $(rootDom); + this.$notes = this.$root.find('.notes'); + this.init(); + } + + SecretItem.prototype.init = function () { + this.$seeNoteBtn = this.$root.find('.notes-trigger'); + this.$seeNoteBtn.on('click', this.toggleNote.bind(this)); + } + + SecretItem.prototype.toggleNote = function () { + this.$notes.toggle('fast'); + } + + //Intialization code + const $secretItems = $(this).find(".secret-item"); + $secretItems.each(function () { + new SecretItem(this) + }) +}); diff --git a/app/assets/javascripts/components/stripe-form.js b/app/assets/javascripts/components/stripe-form.js new file mode 100644 index 0000000..0425ac8 --- /dev/null +++ b/app/assets/javascripts/components/stripe-form.js @@ -0,0 +1,87 @@ +$(function() { + function StripeForm (rootDom, key) { + this.$root = $(rootDom); + this.$cardElement = this.$root.find("#card-element") + this.$errorView = this.$root.find("#card-errors"); + + this.stripe = Stripe(key); + this.elements = this.stripe.elements(); + + this.init(); + } + + StripeForm.prototype.init = function () { + var card = this.initCard(); + this.handleSumbit(card); + this.handleErrors(card); + }; + + StripeForm.prototype.initCard = function () { + var styles = this.cardStyles(); + var card = this.elements.create('card', {style: styles}); + card.mount(this.$cardElement[0]); + return card; + } + + StripeForm.prototype.handleErrors = function (card) { + card.addEventListener("change", function(event) { + if (event.error) { + this.showError(event.error.message); + } else { + this.clearError(); + } + }.bind(this)); + } + + StripeForm.prototype.handleSumbit = function (card) { + this.$root.on("submit", function(event) { + event.preventDefault(); + + this.stripe.createSource(card).then(function(result) { + if (result.error) { + this.showError(result.error.message); + } else { + this.$root.off("submit"); + this.submitSourceResponse(result.source); + } + }.bind(this)); + }.bind(this)); + } + + StripeForm.prototype.submitSourceResponse = function (source) { + // Insert the source ID into the form so it gets submitted to the server + // TODO: Don't we need client_secret in server? + var form = this.$root; + var hiddenInput = document.createElement('input'); + hiddenInput.setAttribute('type', 'hidden'); + hiddenInput.setAttribute('name', 'stripe_source'); + hiddenInput.setAttribute('value', source.id); + form.append(hiddenInput); + + form.submit(); + } + + StripeForm.prototype.showError = function (text) { + this.$errorView.text(text); + }; + + StripeForm.prototype.clearError = function () { + this.$errorView.text(''); + }; + + StripeForm.prototype.cardStyles = function () { + return { + base: { + fontSize: '16px', + color: "#32325d", + } + }; + }; + + //Intialization code + const $newStripeForms = $(this).find(".js-stripe-form"); + $newStripeForms.each(function () { + var key = $(this).data('key'); + new StripeForm(this, key); + }) +}); diff --git a/app/assets/javascripts/components/two-factor-fields.js b/app/assets/javascripts/components/two-factor-fields.js new file mode 100644 index 0000000..b6699b0 --- /dev/null +++ b/app/assets/javascripts/components/two-factor-fields.js @@ -0,0 +1,26 @@ +$(function () { + function TwoFactorFields(rootDom) { + this.$root = $(rootDom); + this.$otpRequiredCheckbox = this.$root.find('.otp-required-for-login'); + this.$enableOtpFields = this.$root.find('.enable-otp-fields'); + this.init(); + } + + TwoFactorFields.prototype.init = function () { + this.$enableOtpFields.toggle(this.isOtpRequired()); + + this.$otpRequiredCheckbox.change(function() { + this.$enableOtpFields.toggle(this.isOtpRequired()); + }.bind(this)); + } + + TwoFactorFields.prototype.isOtpRequired = function () { + return this.$otpRequiredCheckbox.is(':checked'); + } + + //Intialization code + const $hooks = $(this).find(".two-factor-fields"); + $hooks.each(function () { + new TwoFactorFields(this) + }) +}); diff --git a/app/assets/stylesheets/application.sass b/app/assets/stylesheets/application.sass index 0460441..9ff8970 100644 --- a/app/assets/stylesheets/application.sass +++ b/app/assets/stylesheets/application.sass @@ -5,14 +5,35 @@ @import "bootstrap" @import "topsekrit" @import "header-and-footer" +@import "trix" + +// Pages @import "home" @import "secrets" +@import "dashboard" + +// Layouts +@import "layouts/devise" +@import "layouts/settings" + +// Components +@import "components/devise-form" +@import "components/secret-item" +@import "components/two-factor-fields" +@import "components/email-preview" + +// Helpers +@import "helpers" body min-height: 100vh display: flex flex-direction: column + // We still need to do other adjustments + // To change the background color + // background-color: #F2F2F2 + * font-family: "Open Sans", sans-serif @@ -54,4 +75,15 @@ ul.nostyle &:hover background-color: $green3 border-color: $green3 - \ No newline at end of file + +// Shared component styles + +footer + // Keeps footer at the bottom at all times (even if content is short) + margin-top: auto + +.main-header + display: flex + align-items: center + padding: 20px 0 + margin: 0 diff --git a/app/assets/stylesheets/components/devise-form.sass b/app/assets/stylesheets/components/devise-form.sass new file mode 100644 index 0000000..3c341c6 --- /dev/null +++ b/app/assets/stylesheets/components/devise-form.sass @@ -0,0 +1,11 @@ +.devise-form + .heading + margin: 20px 0px + .body + margin: 40px 0px + label + margin-right: 5px + .help-block + display: inline-block + .shared-links + margin: 20px 0px diff --git a/app/assets/stylesheets/components/email-preview.sass b/app/assets/stylesheets/components/email-preview.sass new file mode 100644 index 0000000..ae19212 --- /dev/null +++ b/app/assets/stylesheets/components/email-preview.sass @@ -0,0 +1,18 @@ +.email-preview + padding: 20px + background-color: #fffde0 + border-radius: 5px + +.email-preview__heading + font-size: 1.5rem + border-bottom: $default-border + margin-bottom: 30px + border-color: lightgrey + +.email-preview__body + margin: 15px 0 + +.email-preview__additional-content + margin-top: 30px + font-style: italic + font-weight: 700 diff --git a/app/assets/stylesheets/components/secret-item.sass b/app/assets/stylesheets/components/secret-item.sass new file mode 100644 index 0000000..9735085 --- /dev/null +++ b/app/assets/stylesheets/components/secret-item.sass @@ -0,0 +1,74 @@ +.secret-item + display: flex + flex-direction: column + padding: 10px + border: $default_border + + .header + display: flex + padding: 0 10px + margin-bottom: 5px + .title + font-size: 1.8rem + font-weight: 700px + margin-right: 10px + .viewed-status + padding: 3px 5px + border: $default_border + font-size: 1.3rem + text-transform: uppercase + + .body + display: flex + align-items: center + padding: 10px + justify-content: space-evenly + .emails + display: flex + align-items: center + flex-basis: 40% + padding: 0 5px + .mail-icon + margin-right: 30px + .email-group + div + padding: 3px 0 + .metadata + flex-basis: 40% + padding: 0 5px + div + padding: 3px 0 + .actions + display: flex + flex-direction: column + justify-content: space-around + flex-basis: 20% + padding: 0 5px + text-align: center + .extend-label + border: none + padding: 5px + font-style: italic + .extend-label.extend-btn + border: medium solid + .grouped-actions + display: flex + margin: 10px + justify-content: space-around + .notes-trigger + text-decoration: underline + cursor: pointer + .send-another + text-decoration: underline + + .notes + display: none + background-color: #F2F2F2 + padding: 10px + margin-top: 5px + +@media screen and (max-width: 576px) + .body + flex-direction: column + align-items: flex-start !important + min-height: 230px diff --git a/app/assets/stylesheets/components/two-factor-fields.sass b/app/assets/stylesheets/components/two-factor-fields.sass new file mode 100644 index 0000000..d362968 --- /dev/null +++ b/app/assets/stylesheets/components/two-factor-fields.sass @@ -0,0 +1,17 @@ +.two-factor-fields + margin: 20px 0 + .otp-step + margin: 20px 0 + .qrcode + display: block + width: 250px + height: 250px + border: 20px solid white + .enable-otp-fields + display: none + .instructions + padding: 20px + background-color: #F2F2F2 + .store-icon + width: 30% + margin-right: 10px diff --git a/app/assets/stylesheets/dashboard.sass b/app/assets/stylesheets/dashboard.sass new file mode 100644 index 0000000..fc6e4b4 --- /dev/null +++ b/app/assets/stylesheets/dashboard.sass @@ -0,0 +1,11 @@ +.dashboard + > .header + display: flex + align-items: center + padding: 20px 0 + h2 + display: inline-block + margin: 0 20px 0 0 + .secrets-list + .secret-item + margin: 20px 0 diff --git a/app/assets/stylesheets/header-and-footer.sass b/app/assets/stylesheets/header-and-footer.sass index a9ef4ed..819e77a 100644 --- a/app/assets/stylesheets/header-and-footer.sass +++ b/app/assets/stylesheets/header-and-footer.sass @@ -20,4 +20,17 @@ header.main-nav li:first-child margin-left: 0 +mobile-media-query - font-size: .9em \ No newline at end of file + font-size: .9em + +.dropdown-menu + li + display: block + margin: 0px + +@media screen and (max-width: 576px) + .main-nav + flex-direction: column + align-items: flex-start !important + .main-nav__links + padding: 10px 0 + margin: 0px diff --git a/app/assets/stylesheets/helpers.sass b/app/assets/stylesheets/helpers.sass new file mode 100644 index 0000000..a55e1db --- /dev/null +++ b/app/assets/stylesheets/helpers.sass @@ -0,0 +1,2 @@ +.top-buffer + margin-top: 30px diff --git a/app/assets/stylesheets/layouts/devise.sass b/app/assets/stylesheets/layouts/devise.sass new file mode 100644 index 0000000..459440e --- /dev/null +++ b/app/assets/stylesheets/layouts/devise.sass @@ -0,0 +1,11 @@ +body.devise-layout + min-height: 100vh + display: flex + flex-direction: column + + .container + margin-top: 20px + + .footer + //This makes the footer stay at bottom + margin-top: auto diff --git a/app/assets/stylesheets/layouts/settings.sass b/app/assets/stylesheets/layouts/settings.sass new file mode 100644 index 0000000..05526b3 --- /dev/null +++ b/app/assets/stylesheets/layouts/settings.sass @@ -0,0 +1,34 @@ +@media screen and (max-width: 576px) + body.settings-layout + .sidebar + margin: 0 0 10px 0 !important + +body.settings-layout + min-height: 100vh + display: flex + + h2 + margin-bottom: 1.2rem + + .sidebar + display: flex + flex-direction: column + align-items: flex-end + margin-top: 50px + h2 + font-size: 1.9rem + margin: 15px + ul + display: flex + flex-direction: column + align-items: flex-end + border-right: $default-border + li + list-style-type: none + padding: 10px 15px + a + color: $text-default-color + + .footer + //This makes the footer stay at bottom + margin-top: auto diff --git a/app/assets/stylesheets/secrets.sass b/app/assets/stylesheets/secrets.sass index 59ae768..aa6bd3f 100644 --- a/app/assets/stylesheets/secrets.sass +++ b/app/assets/stylesheets/secrets.sass @@ -12,7 +12,7 @@ margin-bottom: 20px h2 font-size: 1.7em - + .secret__flex-container display: flex +mobile-media-query @@ -43,9 +43,9 @@ background-image: image-url("ol-numbers/2.svg") li.three background-image: image-url("ol-numbers/3.svg") - + li margin-bottom: 10px font-size: 1.1em - line-height: 1.5 \ No newline at end of file + line-height: 1.5 diff --git a/app/assets/stylesheets/topsekrit.scss b/app/assets/stylesheets/topsekrit.scss index 37bde57..98d8b97 100644 --- a/app/assets/stylesheets/topsekrit.scss +++ b/app/assets/stylesheets/topsekrit.scss @@ -31,11 +31,7 @@ form.send-secret-form { } .flash { - margin-bottom: 20px; - span { - margin-left: 15px; - padding: 10px; - } + margin-bottom: 5px; } .field_with_errors .error { diff --git a/app/assets/stylesheets/variables.sass b/app/assets/stylesheets/variables.sass index 82f7978..61b4190 100644 --- a/app/assets/stylesheets/variables.sass +++ b/app/assets/stylesheets/variables.sass @@ -5,12 +5,13 @@ $green2: #1c7279 $green3: #1C5050 $green4: #132D2D +$text-default-color: #333333 $text-grey: #666666 - +$default_border: thin solid #797979 //Sizing $mobile-switch: 675px =mobile-media-query @media(max-width:675px) - @content \ No newline at end of file + @content diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d2e1a8b..1649d56 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,22 +1,8 @@ class ApplicationController < ActionController::Base - protect_from_forgery with: :exception - helper_method :validated_email - - def require_validated_email - redirect_to new_auth_token_path unless validated_email? - end - - def validated_email - session[:validated_email] - end - - def validated_email? - session[:validated_email].present? - end + before_action :configure_permitted_parameters, if: :devise_controller? - def validate_email!(email) - session[:validated_email] = email - end + protect_from_forgery with: :exception + layout :layout_by_resource def notify_exception(exception) if Rails.env.production? @@ -26,4 +12,17 @@ def notify_exception(exception) end end + private + + def layout_by_resource + if devise_controller? + "devise" + else + "application" + end + end + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + end end diff --git a/app/controllers/auth_tokens_controller.rb b/app/controllers/auth_tokens_controller.rb deleted file mode 100644 index 6f629cf..0000000 --- a/app/controllers/auth_tokens_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -class AuthTokensController < ApplicationController - before_action :check_recaptcha, only: :create - - def show - auth_token = AuthToken.find_by(hashed_token: params[:id]) - if auth_token.present? - validate_email!(auth_token.email) - auth_token.delete - redirect_to new_secret_path - else - flash[:message] = "Sorry, we don't know who you are, try sending a new token!" - render :new - end - end - - def new - if validated_email? - # TODO: Remove the HTML formatting and let the view handle it. - flash.now[:message] = "You have already verified your email as #{validated_email}.
- 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 @@ -

- 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" %>. -

diff --git a/app/views/auth_token_mailer/auth_token.text.erb b/app/views/auth_token_mailer/auth_token.text.erb deleted file mode 100644 index 0d874ab..0000000 --- a/app/views/auth_token_mailer/auth_token.text.erb +++ /dev/null @@ -1,11 +0,0 @@ -Hello <%= @email %> - -Here is the requested authentication token that will allow you to access https://SecretLink.org/ - -<%= "#{@request_host}/auth_tokens/#{@token}" %> - -Thank you for using <%= link_to("SecretLink.org", "https://secretlink.org") %>! - - - -If you didn't expect to receive this message, please contact info@secretlink.org diff --git a/app/views/dashboard/_secret_item.erb b/app/views/dashboard/_secret_item.erb new file mode 100644 index 0000000..04169e4 --- /dev/null +++ b/app/views/dashboard/_secret_item.erb @@ -0,0 +1,63 @@ +<% secret %> + +
+
+
<%= secret.title %>
+ <% if secret.consumed_at.blank? %> +
Not Viewed
+ <% end %> +
+
+
+ <% if secret.consumed_at? %> + <%= image_tag 'unlocked-icon.svg', class: 'mail-icon' %> + <% else %> + <%= image_tag 'locked-icon.svg', class: 'mail-icon' %> + <% end %> + +
+ +
+ <% if secret.consumed_at? %> +
Viewed and Deleted
+ <% elsif secret.expired? && !secret.extended?%> + <%= link_to "Extend", extended_secret_path(secret), class: 'extend-label extend-btn', method: :put %> + <% elsif secret.extended? %> +
Extended
+ <% end %> +
+
See Notes
+
|
+
+ <%= link_to "Send Another", new_secret_path(base_id: secret.uuid) %> +
+
+
+
+
<%= secret.comments %>
+
diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb new file mode 100644 index 0000000..32567b2 --- /dev/null +++ b/app/views/dashboard/index.html.erb @@ -0,0 +1,11 @@ +
+
+

All Secrets

+ <%= link_to 'Create New', new_secret_path, class: 'button green' %> +
+
+ <% @secrets.each do |secret| %> + <%= render 'secret_item', secret: secret %> + <% end %> +
+
diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..e8f4415 --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,22 @@ +
+

Resend confirmation instructions

+ +
+ <%= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= f.error_notification %> + <%= f.full_error :confirmation_token %> + +
+ <%= f.input :email, required: true, autofocus: true %> +
+ +
+ <%= f.button :submit, "Resend confirmation instructions" %> +
+ <% end %> +
+ + +
diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..dc55f64 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 0000000..32f4ba8 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 0000000..b41daf4 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..f667dc1 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

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.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..41e148b --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

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

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000..4734c35 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +
+

Set your password

+ +
+ <%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= f.error_notification %> + + <%= f.input :reset_password_token, as: :hidden %> + <%= f.full_error :reset_password_token %> + +
+ <%= f.input :password, label: "New password", required: true, autofocus: true, hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length) %> + <%= f.input :password_confirmation, label: "Confirm your new password", required: true %> +
+ +
+ <%= f.button :submit, "Set my password" %> +
+ <% end %> +
+ + +
diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000..375bb0e --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,21 @@ +
+

Forgot your password?

+ +
+ <%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= f.error_notification %> + +
+ <%= f.input :email, required: true, autofocus: true %> +
+ +
+ <%= f.button :submit, "Send me reset password instructions" %> +
+ <% end %> +
+ + +
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000..5db350b --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,27 @@ +

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 %> + +
+ <%= f.input :email, required: true, autofocus: true %> + + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +

Currently waiting confirmation for: <%= resource.unconfirmed_email %>

+ <% end %> + + <%= f.input :password, autocomplete: "off", hint: "leave it blank if you don't want to change it", required: false %> + <%= f.input :password_confirmation, required: false %> + <%= f.input :current_password, hint: "we need your current password to confirm your changes", required: true %> +
+ +
+ <%= f.button :submit, "Update" %> +
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..55a4fde --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,21 @@ +
+

Sign up

+ +
+ <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= f.error_notification %> + +
+ <%= f.input :email, required: true, autofocus: true %> +
+ +
+ <%= invisible_recaptcha_tags text: 'Sign Up', class: 'btn btn-default' %> +
+ <% end %> +
+ + +
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000..5868952 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,22 @@ +
+

Log in

+ +
+ <%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.input :email, required: false, autofocus: true %> + <%= f.input :password, required: false %> + <%= f.input :otp_attempt, required: false, label: "2FA (for configured accounts)" %> + <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %> +
+ +
+ <%= f.button :submit, "Log in" %> +
+ <% end %> +
+ + +
diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 0000000..e6a3e41 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% 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 %> + +
+ <%= f.input :email, required: true, autofocus: true %> +
+ +
+ <%= f.button :submit, "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/email_template/edit.html.erb b/app/views/email_template/edit.html.erb new file mode 100644 index 0000000..8332893 --- /dev/null +++ b/app/views/email_template/edit.html.erb @@ -0,0 +1,26 @@ +
+
+

Email Template

+ + + + <%= simple_form_for(@settings, url: email_template_path, html: { class:"top-buffer" }) do |f| %> +
+ <%= f.input :send_secret_email_template, label: false, as: :trix_editor %> +
+ +
+ <%= f.button :submit, value: t('buttons.update') %> +
+ <% end %> +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b75556c..195a670 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,7 +6,7 @@ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> - + diff --git a/app/views/layouts/devise.html.erb b/app/views/layouts/devise.html.erb new file mode 100644 index 0000000..f656730 --- /dev/null +++ b/app/views/layouts/devise.html.erb @@ -0,0 +1,25 @@ + + + + SecretLink.org + + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%= csrf_meta_tags %> + + + + + <%= render partial: 'shared/header' %> + <%= render partial: 'shared/flash_messages' %> +
+
+
+ <%= yield %> +
+
+
+ <%= render partial: 'shared/footer' %> + <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + + diff --git a/app/views/layouts/settings.html.erb b/app/views/layouts/settings.html.erb new file mode 100644 index 0000000..30211b0 --- /dev/null +++ b/app/views/layouts/settings.html.erb @@ -0,0 +1,40 @@ + + + + SecretLink.org + + + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%= csrf_meta_tags %> + + + + <%= render partial: 'shared/header' %> + <%= render partial: 'shared/flash_messages' %> +
+
+ +
+ <%= yield %> +
+
+
+ + <%= render partial: 'shared/footer' %> + <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + + diff --git a/app/views/auth_tokens/new.html.erb b/app/views/pages/home.html.erb similarity index 93% rename from app/views/auth_tokens/new.html.erb rename to app/views/pages/home.html.erb index 4ed8a2f..13f99c6 100644 --- a/app/views/auth_tokens/new.html.erb +++ b/app/views/pages/home.html.erb @@ -1,12 +1,15 @@

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 %>
diff --git a/app/views/secret_mailer/secret_notification.html.erb b/app/views/secret_mailer/secret_notification.html.erb index c7a98b5..f1c096b 100644 --- a/app/views/secret_mailer/secret_notification.html.erb +++ b/app/views/secret_mailer/secret_notification.html.erb @@ -3,7 +3,7 @@

- <%= @secret.from_email %> has shared a secret with you via <%= link_to("SecretLink.org", "https://SecretLink.org") %>.
+ <%= sanitize @editable_content %>

<% if @secret.title.present? %> @@ -39,4 +39,3 @@

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 @@ + diff --git a/app/views/secrets/new.html.erb b/app/views/secrets/new.html.erb index 430010f..f11247e 100644 --- a/app/views/secrets/new.html.erb +++ b/app/views/secrets/new.html.erb @@ -3,9 +3,12 @@
- <%= simple_form_for @secret, url: secrets_url, wrapper: 'horizontal_form', html: {class: 'form-horizontal secret__form'} do |f| %> + <%= simple_form_for @secret, url: secrets_url, wrapper: 'horizontal_form', html: {class: 'form-horizontal secret__form js-new-secret-form'} do |f| %>
<%= f.input :title, input_html: { class: 'form-control' } %> +
+ <%= f.input :no_email, label: "Copy the link (don't send email)", input_html: { class: 'no-email-toggle' } %> +
<%= f.input :from_email, label: "Sender:", input_html: { disabled: true, placeholder: from_email_placeholder, class: 'form-control' } %> <%= f.input :to_email, label: "Recipient:", input_html: { placeholder: to_email_placeholder, class: 'form-control' } %> <%= f.input :secret, as: :text, input_html: { class: 'form-control' } %> @@ -28,4 +31,4 @@
-
\ No newline at end of file + diff --git a/app/views/secrets/show.html.erb b/app/views/secrets/show.html.erb index df6302f..ebca175 100644 --- a/app/views/secrets/show.html.erb +++ b/app/views/secrets/show.html.erb @@ -43,7 +43,13 @@ <% end %> <% end %>

- +
+ <% unless current_user.present? %> +
+ Start sending your secret. + <%= link_to 'Register Here', new_user_registration_path %> +
+ <% end %> diff --git a/app/views/shared/_flash_messages.html.erb b/app/views/shared/_flash_messages.html.erb index 51487aa..7fb9e9f 100644 --- a/app/views/shared/_flash_messages.html.erb +++ b/app/views/shared/_flash_messages.html.erb @@ -1,15 +1,10 @@ -<% if flash[:error] %> -
- <%= flash[:error] %> -
-<% end %> -<% if flash[:message] %> +<% flash.each do |type, message| %>
-
-<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/shared/_footer.html.erb b/app/views/shared/_footer.html.erb index 491c0d2..3e28997 100644 --- a/app/views/shared/_footer.html.erb +++ b/app/views/shared/_footer.html.erb @@ -5,9 +5,9 @@