diff --git a/.env.example b/.env.example index be66839..256ffca 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,10 @@ 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='' diff --git a/Gemfile b/Gemfile index e26bcf1..70c1f70 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,12 @@ 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 'sdoc', '~> 0.4.0', group: :doc @@ -41,14 +47,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..a10ef9d 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,24 @@ 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) 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,11 +95,11 @@ 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) @@ -106,6 +120,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,9 +129,11 @@ 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) @@ -142,6 +159,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,6 +170,9 @@ 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) @@ -194,6 +215,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) @@ -245,6 +274,9 @@ GEM 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 +288,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 +314,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 +344,8 @@ DEPENDENCIES show_me_the_cookies (~> 3.1.0) simple_form skylight + 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/icons/locked-icon.svg b/app/assets/images/icons/locked-icon.svg new file mode 100644 index 0000000..4444717 --- /dev/null +++ b/app/assets/images/icons/locked-icon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/app/assets/images/icons/unlocked-icon.svg b/app/assets/images/icons/unlocked-icon.svg new file mode 100644 index 0000000..749a413 --- /dev/null +++ b/app/assets/images/icons/unlocked-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 1ac51c7..1529336 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/header +//= require components/secret-item +//= require components/two-factor-fields +//= require components/new-secret-form diff --git a/app/assets/javascripts/components/header.js b/app/assets/javascripts/components/header.js new file mode 100644 index 0000000..593f9f7 --- /dev/null +++ b/app/assets/javascripts/components/header.js @@ -0,0 +1,25 @@ +$(function() { + // Add smooth scrolling to all header links + $('.main-nav__links a[href^="/home#"], .footer-links a[href^="/home#"]').on('click', function(event) { + smoothScrolling(event); + }); +}); + +function smoothScrolling(event) { + var link = event.target; + + if (link.hash !== "") { + var hash = link.hash; + var target = $(hash).length > 0; + + // Scroll a little higher to account for the sticky header + var headerHeight = 50; + var yValue = $(hash).offset().top - headerHeight; + + if (target) { + $('html, body').animate({ + scrollTop: yValue + }, 800); + } + } +} 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/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 deleted file mode 100644 index 0460441..0000000 --- a/app/assets/stylesheets/application.sass +++ /dev/null @@ -1,57 +0,0 @@ -@import "variables" -@import "pickadate/default" -@import "pickadate/default.date" -@import "bootstrap-sprockets" -@import "bootstrap" -@import "topsekrit" -@import "header-and-footer" -@import "home" -@import "secrets" - -body - min-height: 100vh - display: flex - flex-direction: column - -* - font-family: "Open Sans", sans-serif - -h1,h2,h3,h4,h5,h6,p - line-height: 1.5 - margin-top: 0 - -ul.nostyle - margin: 0 - padding: 0 - list-style: none - -.application-container - max-width: 1200px - width: 100% - margin: 0 auto - padding: 0 20px 50px - flex: 1 0 0 - -.button - display: inline-block - padding: 6px 12px - font-weight: 600 - border-radius: 5px - border: 2px solid transparent - +transition(all linear 50ms) - - &.white - color: white - border-color: white - background-color: transparent - &:hover - color: white - background-color: rgba(255,255,255,.25) - &.green - color: white - border-color: $green2 - background-color: $green2 - &:hover - background-color: $green3 - border-color: $green3 - \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss new file mode 100644 index 0000000..7f6bff9 --- /dev/null +++ b/app/assets/stylesheets/application.scss @@ -0,0 +1,35 @@ +@import "base/variables"; +@import "base/tools"; +@import "pickadate/default"; +@import "pickadate/default.date"; +@import "bootstrap-sprockets"; +@import "bootstrap"; +@import "base/normalize"; +@import "base/body"; +@import "base/typography"; + + +@import "objects/alerts"; +@import "objects/buttons"; + +@import "trix"; + +// Layouts +@import "layouts/dashboard"; +@import "layouts/devise"; +@import "layouts/home"; +@import "layouts/secrets"; +@import "layouts/topsekrit"; + +// Components +@import "components/footer"; +@import "components/header"; +@import "components/banner"; +@import "components/panels"; + +@import "components/two-factor-fields"; +@import "components/email-preview"; +@import "components/steps"; + +// Helpers +@import "base/helpers"; diff --git a/app/assets/stylesheets/base/body.scss b/app/assets/stylesheets/base/body.scss new file mode 100644 index 0000000..f5b0a4a --- /dev/null +++ b/app/assets/stylesheets/base/body.scss @@ -0,0 +1,45 @@ +html, body { + width: 100%; + background-color: white; + font-weight: normal; +} + +body { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +input, textarea, button, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + font-size: 16px; + font-family: $font-family; + line-height: 1.1; + color: $text-default-color; +} + +iframe { + border: 0; +} + +.container-fluid { + padding-left: 0; + padding-right: 0; + padding-bottom: 90px; +} +.row { + margin-left: 0; + margin-right: 0; +} diff --git a/app/assets/stylesheets/base/helpers.scss b/app/assets/stylesheets/base/helpers.scss new file mode 100644 index 0000000..c176fdc --- /dev/null +++ b/app/assets/stylesheets/base/helpers.scss @@ -0,0 +1,7 @@ +.top-buffer { + margin-top: 30px; +} + +.hidden { + display: none; +} diff --git a/app/assets/stylesheets/base/normalize.scss b/app/assets/stylesheets/base/normalize.scss new file mode 100644 index 0000000..192eb9c --- /dev/null +++ b/app/assets/stylesheets/base/normalize.scss @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/app/assets/stylesheets/base/tools.scss b/app/assets/stylesheets/base/tools.scss new file mode 100644 index 0000000..a971b9b --- /dev/null +++ b/app/assets/stylesheets/base/tools.scss @@ -0,0 +1,21 @@ +//Sizing +$mobile-switch: 768px; +$tablet-switch: 1024px; + +@mixin mobile-media-query { + @media(max-width: $mobile-switch) { + @content; + } +} + +@mixin tablet-media-query { + @media(max-width: $tablet-switch) { + @content; + } +} + +@mixin desktop-media-query { + @media(min-width: $mobile-switch) { + @content; + } +} diff --git a/app/assets/stylesheets/base/typography.scss b/app/assets/stylesheets/base/typography.scss new file mode 100644 index 0000000..11e104f --- /dev/null +++ b/app/assets/stylesheets/base/typography.scss @@ -0,0 +1,61 @@ +h1,h2,h3,h4,h5,h6,p { + margin-top: 0; +} + +h2 { + margin-bottom: 1.5em; +} + +h1, h2 { + font-size: 30px; +} + +h3, h4 { + font-size: 24px; +} + +h5, h5 { + font-size: 16px; +} + +p, ul li, ol li, time { + font-size: 14px; + line-height: 1.5; +} + +ul.nostyle { + margin: 0; + padding: 0; + list-style: none; +} + +a { + color: $blue; + text-decoration: underline; + font-size: inherit; + &:hover, &:focus { + text-decoration: none + } +} + +strong, b { + font-size: inherit; + font-weight: bold; +} + +em { + font-size: inherit; + font-weight: inherit; + font-style: italic; +} + + +small { + font-size: 80%; + color: inherit; +} + +span { + font-size: inherit; + color: inherit; +} diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss new file mode 100644 index 0000000..e36ef3a --- /dev/null +++ b/app/assets/stylesheets/base/variables.scss @@ -0,0 +1,20 @@ +//Colors + +$green1: #33A3A2; +$green2: #1C7279; +$green3: #1C5050; +$green4: #132D2D; +$green5: #062B2F; + +$red: #F44336; +$blue: #337AB7; + +$primary-color: $green2; +$text-default-color: #333333; +$text-grey: #666666; +$default_border: thin solid #797979; + + +$font-family: "Open Sans", sans-serif; + +$banner-background: image-url("homepage-hero.jpg"); diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss new file mode 100644 index 0000000..5044f5f --- /dev/null +++ b/app/assets/stylesheets/components/banner.scss @@ -0,0 +1,61 @@ +.banner { + background: black $banner-background no-repeat 50% 50%; + color: white; + background-size: cover; + display: flex; + min-height: 200px; + padding: 50px 20px; + flex-direction: column; + align-items: center; + justify-content: center; + &-large { + min-height: 400px; + h1, h2 { + margin: 0 0 1em; + } + } + h1, h2 { + color: white; + font-weight: bold; + margin: 0; + line-height: 1.2em; + max-width: 700px; + text-align: center; + } + h3 { + color: white; + font-weight: 500; + } + + &_form { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + padding: 0 20px; + width: 100%; + max-width: 500px; + + input[type="email"] { + border: 3px solid black; + height: auto; + padding: 10px 20px; + } + .button.white { + margin-bottom: 1em; + } + .form-group { + width: 100%; + } + .google-link { + color: white; + padding-left: 20px; + background: transparent image-url("homepage-icons/google-g.svg") no-repeat 0 50%; + background-size: 15px; + text-decoration: none; + &:hover, &:focus { + text-decoration: underline; + } + } + } +} diff --git a/app/assets/stylesheets/components/email-preview.scss b/app/assets/stylesheets/components/email-preview.scss new file mode 100644 index 0000000..f8010c5 --- /dev/null +++ b/app/assets/stylesheets/components/email-preview.scss @@ -0,0 +1,27 @@ +.email-preview { + padding: 20px; + background-color: #fffde0; + border-radius: 5px; + margin-bottom: 30px; + + &__heading { + font-size: 1.5rem; + border-bottom: $default-border; + margin-bottom: 30px; + border-color: lightgrey; + } + + &__body { + margin: 15px 0; + } + + &__additional-content { + margin-top: 30px; + font-style: italic; + font-weight: 700; + } +} + +.trix-editor-wrapper { + max-width: 100%; +} diff --git a/app/assets/stylesheets/components/footer.scss b/app/assets/stylesheets/components/footer.scss new file mode 100644 index 0000000..d5f3739 --- /dev/null +++ b/app/assets/stylesheets/components/footer.scss @@ -0,0 +1,44 @@ +.footer { + // Keeps footer at the bottom at all times (even if content is short) + margin-top: auto; + .container-fluid { + padding: 0; + font-size: 90%; + background-color: $green5; + } + + &-content { + padding: 0 3.5em 2.5em 0; + } + + &-logo { + background-color: #fff; + border-radius: 6px; + padding: 1em; + } + &-links { + padding: 1em 1em 1.5em 1em; + li { + display: inline-block; + padding: 0.5em 1em; + line-height: 1.5; + a { + text-transform: uppercase; + } + @include mobile-media-query { + display: block; + } + } + } + .attribution { + margin: 1em 0 0 0; + color: #ddd; + } + a { + color: #fff; + text-decoration: none; + &:hover, &:focus { + text-decoration: underline; + } + } +} diff --git a/app/assets/stylesheets/components/header.scss b/app/assets/stylesheets/components/header.scss new file mode 100644 index 0000000..2f14ccc --- /dev/null +++ b/app/assets/stylesheets/components/header.scss @@ -0,0 +1,68 @@ +.main-header { + display: flex; + align-items: center; + padding: 20px 0; + margin: 0; +} + +.main-nav { + position: sticky; + top: 0; + display: flex; + align-items: center; + background-color: white; + border-bottom: $default_border; + width: 100%; + padding: 0 20px; + z-index: 100; + @include mobile-media-query { + padding: 0 10px; + justify-content: space-between; + } + .navbar-brand { + padding: 15px 15px 15px 0; + white-space: nowrap; + text-decoration: none; + @include mobile-media-query { + height: auto; + padding: 0 10px 0 0; + font-size: 16px; + } + .glyphicon-lg { + font-size: 24px; + font-weight: 500; + vertical-align: top; + padding-right: 0.5em; + line-height: 0.75; + } + } + + &__links { + margin: 0 0 0 auto; + padding: 0; + list-style: none; + text-align: right; + @include mobile-media-query { + font-size: .9em; + padding: 10px 0; + margin: 0px; + } + li { + display: inline-block; + margin-left: 10px; + &:first-child { + margin-left: 0; + } + a { + text-decoration: none; + } + } + } +} + +.dropdown-menu { + li { + display: block; + margin: 0px; + } +} diff --git a/app/assets/stylesheets/components/panels.scss b/app/assets/stylesheets/components/panels.scss new file mode 100644 index 0000000..6758b58 --- /dev/null +++ b/app/assets/stylesheets/components/panels.scss @@ -0,0 +1,56 @@ +.panel { + display: flex; + flex-direction: column; + justify-content: space-between; + max-width: 500px; + border: $default_border; + margin: 0 auto 40px; + &.large { + max-width: 600px; + } + + + &__header { + background-color: $primary-color; + padding: 15px; + text-align: center; + &.light-green { + background-color: $green1; + } + &.mail-icon { + display: flex; + justify-content: space-between; + align-items: center; + text-align: left; + img { + width: 62px; + } + div { + width: calc(100% - 70px); + } + } + p.viewed-status { + color: white; + margin: 10px 0 0 0; + } + } + + &__title { + color: white; + font-size: 24px; + margin-bottom: 0; + } + + &__body { + flex-grow: 1; + padding: 20px; + } + + &__footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 20px; + border-top: $default_border; + } +} diff --git a/app/assets/stylesheets/components/steps.scss b/app/assets/stylesheets/components/steps.scss new file mode 100644 index 0000000..40584ed --- /dev/null +++ b/app/assets/stylesheets/components/steps.scss @@ -0,0 +1,166 @@ +.homepage-steps { + display: flex; + width: 100%; + padding: 50px 20px; + justify-content: center; + background: black image-url("homepage-features-bg.jpg"); + @include mobile-media-query { + flex-direction: column; + align-items: center; + } + &__step { + width: 225px; + background: white; + display: flex; + flex-direction: column; + border-radius: 5px 5px 0 0; + @include mobile-media-query { + width: 100%; + position: relative; + border-radius: 0; + } + header { + color: white; + text-align: center; + padding: 30px 20px; + height: 160px; + position: relative; + border-radius: 5px 5px 0 0; + @include mobile-media-query { + position: static; + border-radius: 0; + height: auto; + display: flex; + align-items: center; + padding-bottom: 0; + } + + &:after { + left: 100%; + top: 50%; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border: solid transparent; + border-color: rgba(136, 183, 213, 0); + border-width: 30px; + margin-top: -55px; + z-index: 50; + @include mobile-media-query { + top: 100%; + left: 50%; + margin-left: -30px; + margin-top: 0; + border: transparent solid; + border-width: 30px; + border-left-color: transparent !important; + border-top-color: $green1; + } + } + + h3 { + font-size: 3.5em; + font-weight: 600; + margin: 0 0 10px; + line-height: 1em; + color: white; + @include mobile-media-query { + margin: 0 10px 0 0; + } + } + h4 { + font-size: 1em; + font-weight: 600; + margin: 0; + color: white; + } + } + + .body { + padding: 20px; + border-right: 1px solid #ccc; + flex: 1 0 0; + img { + height: 65px; + max-width: 70px; + margin: 0 auto 20px; + display: block; + @include mobile-media-query { + display: none; + } + } + p { + @include mobile-media-query { + color: white; + } + } + } + + &.first { + border-radius: 5px 5px 0 5px; + @include mobile-media-query { + border-radius: 5px 5px 0 0; + background-color: $green1; + } + + header { + background-color: $green1; + @include mobile-media-query { + border-radius: 5px 5px 0 0; + } + &:after { + border-left-color: $green1; + @include mobile-media-query { + border-top-color: $green1; + } + } + } + } + + &.second { + @include mobile-media-query { + background-color: $green2; + } + header { + background-color: $green2; + &:after { + border-left-color: $green2; + @include mobile-media-query { + border-top-color: $green2; + } + } + } + } + &.third { + @include mobile-media-query { + background-color: $green3; + } + header { + background-color: $green3; + &:after { + display: none; + @include mobile-media-query { + border-top-color: $green3; + } + } + } + } + + &.fourth { + border-radius: 5px 5px 5px 0; + @include mobile-media-query { + border-radius: 0 0 5px 5px; + background-color: $green4; + } + .body { + border-right: none; + @include mobile-media-query { + border: none; + color: white; + } + } + } + } +} diff --git a/app/assets/stylesheets/components/two-factor-fields.scss b/app/assets/stylesheets/components/two-factor-fields.scss new file mode 100644 index 0000000..1297401 --- /dev/null +++ b/app/assets/stylesheets/components/two-factor-fields.scss @@ -0,0 +1,31 @@ +.two-factor-fields { + .form-group.boolean { + width: 100%; + text-align: left; + .checkbox { + cursor: pointer; + @include mobile-media-query { + width: 100%; + } + label { + width: auto; + } + } + @include mobile-media-query { + width: auto; + } + } + .otp-step { + margin: 20px 0; + } + .qrcode { + display: block; + width: 250px; + height: 250px; + border: 20px solid white; + margin: 0 auto; + } + .enable-otp-fields { + display: none; + } +} diff --git a/app/assets/stylesheets/header-and-footer.sass b/app/assets/stylesheets/header-and-footer.sass deleted file mode 100644 index a9ef4ed..0000000 --- a/app/assets/stylesheets/header-and-footer.sass +++ /dev/null @@ -1,23 +0,0 @@ -header.main-nav - width: 100% - display: flex - max-width: 1200px - margin: 0 auto - align-items: center - padding: 0 20px - .navbar-brand - padding: 15px 15px 15px 0 - white-space: nowrap - -.main-nav__links - margin: 0 0 0 auto - padding: 0 - list-style: none - text-align: right - li - display: inline-block - margin-left: 10px - li:first-child - margin-left: 0 - +mobile-media-query - font-size: .9em \ No newline at end of file diff --git a/app/assets/stylesheets/home.sass b/app/assets/stylesheets/home.sass deleted file mode 100644 index 1dc3243..0000000 --- a/app/assets/stylesheets/home.sass +++ /dev/null @@ -1,256 +0,0 @@ -.homepage-hero - background: black image-url("homepage-hero.jpg") no-repeat 50% 50% - background-size: cover - display: flex - min-height: 400px - padding: 50px 20px - flex-direction: column - align-items: center - justify-content: center - h2 - color: white - font-weight: bold - margin: 0 0 1em - line-height: 1.2em - max-width: 700px - text-align: center - h3 - color: white - font-weight: 500 - form - display: flex - justify-content: center - flex-direction: column - align-items: center - padding: 0 20px - width: 100% - max-width: 500px - .hidden - display: none - input[type="email"] - border: 3px solid black - height: auto - padding: 10px 20px - .button.white - margin-bottom: 1em - .form-group - width: 100% - .google-link - color: white - padding-left: 20px - background: transparent image-url("homepage-icons/google-g.svg") no-repeat 0 50% - background-size: 15px - &:hover - text-decoration: underline - -.homepage-features ul.nostyle - display: flex - justify-content: center - width: 100% - padding: 50px 20px - li - max-width: 350px - text-align: center - padding: 20px - img - max-height: 50px - margin-bottom: 20px - h3 - font-size: 1em - color: $green2 - font-weight: 700 - p - color: $text-grey - +mobile-media-query - flex-direction: column - align-items: center - img - width: 100% - -.homepage-steps - display: flex - width: 100% - padding: 50px 20px - justify-content: center - background: black image-url("homepage-features-bg.jpg") - -.homepage-steps__step - width: 225px - background: white - display: flex - flex-direction: column - border-radius: 5px 5px 0 0 - header - color: white - text-align: center - padding: 30px 20px - height: 160px - position: relative - border-radius: 5px 5px 0 0 - &:after - left: 100% - top: 50% - content: " " - height: 0 - width: 0 - position: absolute - pointer-events: none - border: solid transparent - border-color: rgba(136, 183, 213, 0) - border-width: 30px - margin-top: -55px - z-index: 50 - h3 - font-size: 3.5em - font-weight: 600 - margin: 0 0 10px - line-height: 1em - h4 - font-size: 1em - font-weight: 600 - margin: 0 - &.first header - background-color: $green1 - &:after - border-left-color: $green1 - &.second header - background-color: $green2 - &:after - border-left-color: $green2 - &.third header - background-color: $green3 - &:after - display: none - - .body - padding: 20px - border-right: 1px solid #ccc - flex: 1 0 0 - img - height: 65px - max-width: 70px - margin: 0 auto 20px - display: block - &.first - border-radius: 5px 5px 0 5px - &.fourth - border-radius: 5px 5px 5px 0 - .body - border-right: none - -+mobile-media-query - .homepage-steps - flex-direction: column - align-items: center - .homepage-steps__step - width: 100% - position: relative - border-radius: 0 - header - position: static - border-radius: 0 - height: auto - display: flex - align-items: center - padding-bottom: 0 - h3 - margin: 0 10px 0 0 - header:after - top: 100% - left: 50% - margin-left: -30px - margin-top: 0 - border: transparent solid - border-width: 30px - border-left-color: transparent !important - border-top-color: $green1 - &.first - border-radius: 5px 5px 0 0 - background-color: $green1 - header - border-radius: 5px 5px 0 0 - header:after - border-top-color: $green1 - &.second - background-color: $green2 - header:after - border-top-color: $green2 - &.third - background-color: $green3 - header:after - border-top-color: $green3 - &.fourth - border-radius: 0 0 5px 5px - background-color: $green4 - .body - border: none - color: white - .body img - display: none - - - -.homepage-behind-the-scenes - text-align: center - padding: 60px 20px - color: $text-grey - h2 - font-weight: 600 - margin-bottom: 10px - p - margin-bottom: 20px - -.homepage-faq - background-color: $green2 - padding: 50px 20px - color: white - .flex-wrapper - max-width: 900px - margin: 0 auto - display: flex - flex-wrap: wrap - justify-content: center - a - color: white - text-decoration: underline - &:hover - color: $green1 - -.homepage-faq__question - width: 50% - min-width: 310px - padding: 20px - h3 - font-weight: 600 - +mobile-media-query - width: 100% - padding: 0 20px - h3[data-slideTarget] - font-size: 1.3em - padding-left: 25px - background: transparent image-url("icons/plus-white.svg") no-repeat 0 50% - background-size: 15px - cursor: pointer - div[data-slide] - display: none - margin-bottom: 20px - -.homepage-about-us - padding: 50px 20px - .flex-wrapper - max-width: 900px - margin: 0 auto - display: flex - flex-wrap: wrap - justify-content: center - h3.full-width - width: 100% - font-weight: 600 - padding: 0 20px - .flex-column - width: 50% - min-width: 310px - padding: 20px - +mobile-media-query - width: 100% - padding: 0 20px diff --git a/app/assets/stylesheets/layouts/dashboard.scss b/app/assets/stylesheets/layouts/dashboard.scss new file mode 100644 index 0000000..e43e70f --- /dev/null +++ b/app/assets/stylesheets/layouts/dashboard.scss @@ -0,0 +1,72 @@ +.dashboard { + padding: 40px 0; + &__secrets-list { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + @include mobile-media-query { + flex-direction: column; + } + .secret-item { + @include desktop-media-query { + display: grid; + grid-template-columns: 1fr 2fr 160px; + flex-direction: row; + width: 100%; + max-width: 100%; + margin-bottom: 20px; + } + @include mobile-media-query { + width: 90%; + margin: 0 auto 20px; + flex-direction: column; + } + .panel__header { + @include desktop-media-query { + background: linear-gradient(to right, $primary-color, $primary-color 95px, white 95px); + } + &.light-green { + @include desktop-media-query { + background: linear-gradient(to right, $green1, $green1 95px, white 95px); + } + } + .panel__title, p.viewed-status { + @include desktop-media-query { + color: $text-default-color; + margin-left: 20px; + } + } + } + .panel__body { + @include desktop-media-query { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 10px; + } + p { + @include desktop-media-query { + width: 30%; + word-break: break-word; + } + @include tablet-media-query { + width: auto; + } + } + .notes { + display: none; + } + } + .panel__footer { + @include desktop-media-query { + flex-direction: column; + justify-content: space-around; + text-align: center; + border-top: 0; + padding: 10px; + } + } + } + } +} diff --git a/app/assets/stylesheets/layouts/devise.scss b/app/assets/stylesheets/layouts/devise.scss new file mode 100644 index 0000000..adb4ed9 --- /dev/null +++ b/app/assets/stylesheets/layouts/devise.scss @@ -0,0 +1,76 @@ +$label-width: 120px; + +.devise_page { + width: 80%; + max-width: 1200px; + margin: 40px auto; + @include mobile-media-query { + width: 95% + } + &.two-column { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + @include mobile-media-query { + flex-direction: column; + } + .panel, .secret__instructions { + width: 48%; + max-width: 100%; + @include mobile-media-query { + width: 95%; + margin: 20px auto; + } + &.large { + width: auto; + } + } + } + + &__form { + .form-group { + display: flex; + flex-direction: row-reverse; + align-items: center; + flex-wrap: wrap; + + label { + width: $label-width; + text-align: right; + font-weight: bold; + cursor: pointer; + margin-right: 10px; + } + .form-control { + flex-grow: 1; + width: auto; + } + .checkbox { + width: calc(100% - #{$label-width} + 10px); + label { + width: auto; + } + } + .help-block { + width: 100%; + } + } + + .form-actions { + margin-left: calc(#{$label-width} + 10px); + } + } + + &__links { + width: 100%; + text-align: center; + a { + display: inline-block; + padding: 0 1em; + border-right: $default_border; + &:last-child { + border-right: 0; + } + } + } +} diff --git a/app/assets/stylesheets/layouts/home.scss b/app/assets/stylesheets/layouts/home.scss new file mode 100644 index 0000000..465db36 --- /dev/null +++ b/app/assets/stylesheets/layouts/home.scss @@ -0,0 +1,131 @@ +.application-container { + width: 80%; + max-width: 1200px; + margin: 40px auto; + @include mobile-media-query { + width: 95% + } +} + +.homepage-features ul.nostyle { + display: flex; + justify-content: center; + width: 100%; + padding: 50px 20px; + @include mobile-media-query { + flex-direction: column; + align-items: center; + } + + li { + max-width: 350px; + text-align: center; + padding: 20px; + } + img { + max-height: 50px; + margin-bottom: 20px; + @include mobile-media-query { + width: 100%; + } + } + h3 { + font-size: 1em; + color: $green2; + font-weight: 700; + } + p { + color: $text-grey; + } +} + +.homepage-behind-the-scenes { + text-align: center; + padding: 60px 20px; + color: $text-grey; + h2 { + font-weight: 600; + margin-bottom: 10px; + color: currentColor; + } + p { + margin-bottom: 20px; + color: currentColor; + } +} + +.homepage-faq { + background-color: $green2; + padding: 50px 20px; + color: white; + .flex-wrapper { + max-width: 900px; + margin: 0 auto; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + a { + color: white; + text-decoration: underline; + &:hover, &:focus { + color: $green1; + } + } + &__question { + width: 50%; + min-width: 310px; + padding: 20px; + color: white; + @include mobile-media-query { + width: 100%; + padding: 0 20px; + } + + h3[data-slideTarget] { + font-weight: 600; + color: currentColor; + @include mobile-media-query { + font-size: 1.3em; + padding-left: 25px; + background: transparent image-url("icons/plus-white.svg") no-repeat 0 50%; + background-size: 15px; + cursor: pointer; + } + } + div[data-slide] { + @include mobile-media-query { + display: none; + margin-bottom: 20px; + } + p { + color: white; + } + } + } +} + +.homepage-about-us { + padding: 50px 20px; + .flex-wrapper { + max-width: 900px; + margin: 0 auto; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + h3.full-width { + width: 100%; + font-weight: 600; + padding: 0 20px; + } + .flex-column { + width: 50%; + min-width: 310px; + padding: 20px; + @include mobile-media-query { + width: 100%; + padding: 0 20px; + } + } +} diff --git a/app/assets/stylesheets/layouts/secrets.scss b/app/assets/stylesheets/layouts/secrets.scss new file mode 100644 index 0000000..c717dff --- /dev/null +++ b/app/assets/stylesheets/layouts/secrets.scss @@ -0,0 +1,56 @@ +.secret { + &__flex-container { + display: flex; + @include mobile-media-query { + flex-wrap: wrap; + flex-direction: column-reverse; + } + } + + &__form { + width: 100%; + padding: 0 20px; + } + + &__instructions { + width: 100%; + max-width: 300px; + margin: 0 0 0 20px; + @include mobile-media-query { + margin: 0 0 20px; + max-width: 100%; + } + ol { + background: #e6ebec; + border: 3px solid #ced9db; + padding: 15px; + list-style: none; + li { + padding-left: 25px; + background: transparent image-url("ol-numbers/1.svg") no-repeat 0 2px; + background-size: 20px 20px; + &.two { + background-image: image-url("ol-numbers/2.svg"); + } + &.three { + background-image: image-url("ol-numbers/3.svg"); + } + } + } + + li { + margin-bottom: 10px; + font-size: 1.1em; + line-height: 1.5; + a { + text-decoration: none; + } + .store-icon { + margin-right: 10px; + @include mobile-media-query { + margin-bottom: 10px; + } + } + } + } +} diff --git a/app/assets/stylesheets/topsekrit.scss b/app/assets/stylesheets/layouts/topsekrit.scss similarity index 57% rename from app/assets/stylesheets/topsekrit.scss rename to app/assets/stylesheets/layouts/topsekrit.scss index 37bde57..5eb31bd 100644 --- a/app/assets/stylesheets/topsekrit.scss +++ b/app/assets/stylesheets/layouts/topsekrit.scss @@ -1,26 +1,3 @@ -@import url(https://fonts.googleapis.com/css?family=Oxygen:400,700,300); - -$primary-color: #1C7279; - -body { - font-family: 'Oxygen', sans-serif; -} -.container-fluid { - padding-left: 0; - padding-right: 0; - padding-bottom: 90px; -} -.row { - margin-left: 0; - margin-right: 0; -} -h2 { - margin-bottom: 1.5em; -} -a:hover { - text-decoration: none; -} - form.send-secret-form { abbr { display: none; @@ -30,62 +7,6 @@ form.send-secret-form { // } } -.flash { - margin-bottom: 20px; - span { - margin-left: 15px; - padding: 10px; - } -} - -.field_with_errors .error { - color: red; -} - -/***** NAVBAR *****/ - -.navbar { - background-color: #fff; - margin-bottom: 0; - padding: 0.75em 0; - font-size: 20px; - border: none; -} -.navbar .navbar-brand, -.glyphicon-lg { - font-size: 24px; - font-weight: 500; - vertical-align: top; -} -.glyphicon-lg { - padding-right: 0.5em; - line-height: 0.75; -} -.navbar-default .navbar-brand, -.navbar-default .navbar-nav > li > a { - color: $primary-color; -} -.navbar-toggle { - border: none; -} -.navbar-default .navbar-toggle:hover, -.navbar-default .navbar-toggle:focus { - background-color: #fff; -} -.navbar-default .navbar-toggle .icon-bar { - background-color: $primary-color; -} - -.navbar { - a.github { - line-height: 1.5; - display: none; - } - img { - height: 32px; - width: 32px; - } -} /***** INTRODUCTION *****/ @@ -131,21 +52,7 @@ form.send-secret-form { line-height: 3; } -.btn-lead-outline { - margin: 1.5em 0; -} -.btn-lead { - background-color: $primary-color; - border-radius: 6px; - padding: 0.5em 2em; - color: #fff; - cursor: pointer; -} -.btn-lead:hover { - background-color: #fff; - color: $primary-color; - font-weight: 400; -} + .auth-form { background-color: #fff; margin: 3em 0; @@ -246,43 +153,6 @@ form.send-secret-form { text-decoration: underline; } -/***** FOOTER *****/ - -.footer .container-fluid { - padding: 0; - font-size: 90%; - background-color: #062B2F; -} -.footer-content { - padding: 0 3.5em 2.5em 0; -} -.footer-logo { - background-color: #fff; - border-radius: 6px; - padding: 1em; -} -.footer-links { - padding: 1em 1em 1.5em 1em; - li { - display: inline-block; - padding: 0.5em 1em; - line-height: 1.5; - } -} - -.attribution { - margin: 1em 0 0 0; - color: #ddd; - a { - color: #fff; - } -} - -.footer-links a { - color: #fff; - text-transform: uppercase; -} - /***** SECRET *****/ div.secret_secret textarea, div.secret_comments textarea { diff --git a/app/assets/stylesheets/objects/alerts.scss b/app/assets/stylesheets/objects/alerts.scss new file mode 100644 index 0000000..c332b4a --- /dev/null +++ b/app/assets/stylesheets/objects/alerts.scss @@ -0,0 +1,9 @@ +.flash { + position: absolute; + width: 100%; + margin-bottom: 0; +} + +.field_with_errors .error { + color: $red; +} diff --git a/app/assets/stylesheets/objects/buttons.scss b/app/assets/stylesheets/objects/buttons.scss new file mode 100644 index 0000000..cb9a326 --- /dev/null +++ b/app/assets/stylesheets/objects/buttons.scss @@ -0,0 +1,70 @@ +.button { + display: inline-block; + padding: 6px 12px; + font-weight: 600; + border-radius: 5px; + border: 2px solid transparent; + text-decoration: none; + @include transition(all linear 50ms); + + &.white { + color: white; + border-color: white; + background-color: transparent; + &:hover, &:focus { + color: white; + background-color: rgba(255,255,255,.25); + } + } + &.green { + color: white; + border-color: $green2; + background-color: $green2; + &:hover, &:focus { + background-color: $green3; + border-color: $green3; + } + } + &.blue { + color: white; + border-color: $blue; + background-color: $blue; + &:hover, &:focus { + background-color: darken($blue, 20%); + border-color: darken($blue, 20%); + } + } + &.red { + color: white; + border-color: $red; + background-color: $red; + &:hover, &:focus { + background-color: darken($red, 20%); + border-color: darken($red, 20%); + } + } + &.notes-trigger { + padding: 3px 6px; + font-size: 14px; + border-color: $blue; + color: $blue; + } +} + +.btn-lead { + background-color: $primary-color; + color: #fff; + border-radius: 6px; + padding: 0.5em 2em; + + font-weight: 400; + cursor: pointer; + &:hover, &:focus { + background-color: #fff; + color: $primary-color; + } + + &-outline { + margin: 1.5em 0; + } +} diff --git a/app/assets/stylesheets/secrets.sass b/app/assets/stylesheets/secrets.sass deleted file mode 100644 index 59ae768..0000000 --- a/app/assets/stylesheets/secrets.sass +++ /dev/null @@ -1,51 +0,0 @@ -.secret__title - width: 100% - padding: 50px 20px - background: black image-url("homepage-hero.jpg") no-repeat 50% 50% - background-size: cover - text-align: center - margin-bottom: 50px - h2 - color: white - margin: 0 - +mobile-media-query - margin-bottom: 20px - h2 - font-size: 1.7em - -.secret__flex-container - display: flex - +mobile-media-query - flex-wrap: wrap - flex-direction: column-reverse - -.secret__form - width: 100% - padding: 0 20px - -.secret__instructions - width: 100% - max-width: 300px - margin: 0 0 0 20px - +mobile-media-query - margin: 0 0 20px - max-width: 100% - ol - background: #e6ebec - border: 3px solid #ced9db - padding: 15px - list-style: none - li - padding-left: 25px - background: transparent image-url("ol-numbers/1.svg") no-repeat 0 2px - background-size: 20px 20px - li.two - 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 diff --git a/app/assets/stylesheets/variables.sass b/app/assets/stylesheets/variables.sass deleted file mode 100644 index 82f7978..0000000 --- a/app/assets/stylesheets/variables.sass +++ /dev/null @@ -1,16 +0,0 @@ -//Colors - -$green1: #33A3A2 -$green2: #1c7279 -$green3: #1C5050 -$green4: #132D2D - -$text-grey: #666666 - - -//Sizing -$mobile-switch: 675px - -=mobile-media-query - @media(max-width:675px) - @content \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d2e1a8b..02de5c0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,22 +1,7 @@ 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 + before_action :configure_permitted_parameters, if: :devise_controller? - def validated_email - session[:validated_email] - end - - def validated_email? - session[:validated_email].present? - end - - def validate_email!(email) - session[:validated_email] = email - end + protect_from_forgery with: :exception def notify_exception(exception) if Rails.env.production? @@ -26,4 +11,9 @@ def notify_exception(exception) end end + private + + 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..d50d745 100644 --- a/app/controllers/decrypted_secrets_controller.rb +++ b/app/controllers/decrypted_secrets_controller.rb @@ -1,11 +1,10 @@ class DecryptedSecretsController < ApplicationController include RetrieveSecret before_filter :retrieve_secret - before_filter :require_validated_email def create begin - @unencrypted_secret = SecretService.decrypt_secret!(@secret, params[:key]) + @unencrypted_secret = SecretService.decrypt_secret!(@secret, params[:key], activity_logger) rescue => e notify_exception(e) flash.now[:error] = "An error occurred while trying to decrypt the secret, please ask #{@secret.from_email} to send it again." @@ -13,4 +12,9 @@ def create render 'secrets/show' end + def activity_logger + user = User.find_by(email: @secret.from_email) + @activity_logger ||= ActivityLogger.new(user) + end + end diff --git a/app/controllers/email_template_controller.rb b/app/controllers/email_template_controller.rb new file mode 100644 index 0000000..5e9bd41 --- /dev/null +++ b/app/controllers/email_template_controller.rb @@ -0,0 +1,27 @@ +class EmailTemplateController < AuthenticatedController + 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.instance_eval { 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..b353300 --- /dev/null +++ b/app/controllers/extended_secrets_controller.rb @@ -0,0 +1,14 @@ +class ExtendedSecretsController < AuthenticatedController + def update + @secret = current_user.secrets.find(params[:id]) + + if @secret.expired? && !@secret.extended? + @secret.extend_expiry! + ActivityLogger.new(current_user).perform(Secret::ACTIVITY_LOG_KEYS[:extended], @secret) + 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..c068b73 100644 --- a/app/controllers/secrets_controller.rb +++ b/app/controllers/secrets_controller.rb @@ -1,37 +1,65 @@ -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, + activity_logger + ) + 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 + def activity_logger + @activity_logger ||= ActivityLogger.new(current_user) + 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..4db5cc0 --- /dev/null +++ b/app/controllers/two_factor_auth_controller.rb @@ -0,0 +1,42 @@ +class TwoFactorAuthController < AuthenticatedController + 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..889a39a 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.instance_eval { binding } + ).run + end end diff --git a/app/models/activity_log.rb b/app/models/activity_log.rb new file mode 100644 index 0000000..b612461 --- /dev/null +++ b/app/models/activity_log.rb @@ -0,0 +1,17 @@ +class ActivityLog < ActiveRecord::Base + validates :key, :owner, :trackable, presence: true + validate :valid_key + + belongs_to :owner, polymorphic: true + belongs_to :trackable, polymorphic: true + belongs_to :recipient, polymorphic: true + + private + + def valid_key + if key.present? && trackable.present? + trackable_keys = trackable.class::ACTIVITY_LOG_KEYS.values + errors.add(:key, :invalid) unless trackable_keys.include?(key) + end + 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..811b3ef 100644 --- a/app/models/secret.rb +++ b/app/models/secret.rb @@ -1,4 +1,7 @@ class Secret < ActiveRecord::Base + ACTIVITY_LOG_KEYS = { created: 'created', consumed: 'consumed', + extended: 'extended', deleted: 'deleted' }.freeze + attr_accessor :secret_key attr_encrypted :secret, key: :secret_key, mode: :per_attribute_iv_and_salt @@ -9,8 +12,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 +26,44 @@ class Secret < ActiveRecord::Base where('from_email = ? and access_key = ?', email, access_key) } + belongs_to :user, primary_key: 'email', foreign_key: 'from_email' + has_many :activity_logs, as: :trackable + + 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 +89,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/user.rb b/app/models/user.rb new file mode 100644 index 0000000..e537a31 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,39 @@ +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' + has_many :activities, as: :owner, class_name: 'ActivityLog' + + 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/activity_logger.rb b/app/services/activity_logger.rb new file mode 100644 index 0000000..93966fd --- /dev/null +++ b/app/services/activity_logger.rb @@ -0,0 +1,22 @@ +class ActivityLogger + attr_reader :owner + + def initialize(owner) + @owner = owner + end + + def perform(key, trackable, recipient = nil) + # Right now this is just a method call wrapped in a class + # But still we're doing it to hide the implementation details + # We may consider to change logging in the future + # And this is the only place we will go to + + # And also, it pays to make sure that we're always calling the bang(!) + # This step should not fail in production + ActivityLog.create!( + key: key, + owner: owner, + trackable: trackable, + recipient: recipient) + end +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..ee5b7b3 100644 --- a/app/services/secret_service.rb +++ b/app/services/secret_service.rb @@ -1,10 +1,18 @@ class SecretService - def self.encrypt_new_secret(params) + # TODO: Instantiate the service instead and assign the logger + # as instance property + def self.encrypt_new_secret(params, email_template = nil, logger = nil) secret = Secret.create(params.merge(uuid: SecureRandom.uuid, secret_key: SecureRandom.hex(16))) - if secret.persisted? + if secret.persisted? && !secret.no_email? + + logger.perform(Secret::ACTIVITY_LOG_KEYS[:created], secret) if logger + # TODO: Mailers should be in the background - SecretMailer.secret_notification(secret).deliver_now + SecretMailer.secret_notification( + secret, + email_template + ).deliver_now end secret end @@ -20,11 +28,21 @@ def self.correct_key?(secret, password) end end - def self.decrypt_secret!(secret, password) + def self.decrypt_secret!(secret, password, logger = nil) secret.secret_key = password s = secret.secret - secret.delete_encrypted_information secret.mark_as_consumed + secret.delete_encrypted_information + + if logger + # In this case, this happens at almost the same time + # Later we will have a script that deletes expired + # but unconsumed secrets + logger.perform(Secret::ACTIVITY_LOG_KEYS[:consumed], secret) + logger.perform(Secret::ACTIVITY_LOG_KEYS[:deleted], secret) + end + + # TODO: Mailers should be in the background SecretMailer.consumnation_notification(secret).deliver_now s 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..656c710 --- /dev/null +++ b/app/views/dashboard/_secret_item.erb @@ -0,0 +1,67 @@ +<% secret %> + +
+ +
+ <% if secret.consumed_at? %> + <%= image_tag 'icons/unlocked-icon.svg', alt: 'Unlocked email', width: 62 %> + <% else %> + <%= image_tag 'icons/locked-icon.svg', alt: 'Locked Mail', width: 62 %> + <% end %> + +
+

<%= secret.title %>

+ <% if secret.consumed_at.blank? %> +

Not Viewed

+ <% else %> +

Viewed

+ <% end %> +
+
+ +
+ <% if secret.no_email? %> +

+ This secret was sent manually + (no email sent) +

+ <% else %> +

From: <%= secret.from_email %>

+

To: <%= secret.to_email %>

+ <% end %> + + +

+ <% if secret.no_email? %> + Created: <%= local_time(secret.created_at, t('date.formats.dashboard_time')) %> + <% else %> + Sent: <%= local_time(secret.sent_at, t('date.formats.dashboard_time')) %> + <% end %> +

+ +

+ <% if secret.consumed_at? %> + Viewed: <%= local_time(secret.consumed_at, t('date.formats.dashboard_time')) %> + <% elsif secret.expired? %> + Not viewed: expired: <%= local_time(secret.expire_at, t('date.formats.dashboard_time')) %> + <% else %> + Not viewed: expiry: <%= local_time(secret.expire_at, t('date.formats.dashboard_time')) %> + <% end %> +

+

+ + <%= secret.comments %> +

+

+ +
+ <% if secret.consumed_at? %> +
Viewed and Deleted
+ <% elsif secret.expired? && !secret.extended?%> + <%= link_to "Extend", extended_secret_path(secret), class: 'extend-btn button blue', method: :put %> + <% elsif secret.extended? %> +
Extended
+ <% end %> + <%= link_to "Send Another".html_safe, new_secret_path(base_id: secret.uuid), class: 'button green' %> +
+
diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb new file mode 100644 index 0000000..7757b77 --- /dev/null +++ b/app/views/dashboard/index.html.erb @@ -0,0 +1,12 @@ + + +
+
+ <% @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..37362cb --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,27 @@ + + +
+
+
+

Resend confirmation instructions

+
+
+ <%= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: "devise_page__form" }) do |f| %> + <%= f.error_notification %> + <%= f.full_error :confirmation_token %> + + <%= f.input :email, required: true, autofocus: true %> + +
+ <%= f.button :submit, "Resend", class: "button green" %> +
+ <% 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..c2f8e59 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,31 @@ + + +
+
+
+

Set your password

+
+
+ + <%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: "devise_page__form" }) 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 password", class: "button green" %> +
+ <% 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..51509ab --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,26 @@ + + +
+
+
+

Forgot your password?

+
+
+ <%= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: "devise_page__form" }) do |f| %> + <%= f.error_notification %> + + <%= f.input :email, required: true, autofocus: true %> + +
+ <%= f.button :submit, "Reset password", class: "button green" %> +
+ <% 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..a7c2fa9 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,38 @@ + + +
+
+
+

Edit <%= resource_name.to_s.humanize %>

+
+
+ <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: "devise_page__form" }) 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", class: "button green" %> +
+ <% end %> +
+
+ + +
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..1c1012c --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,26 @@ + + +
+
+
+

New User? Create an Account

+
+
+ <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "devise_page__form"}) do |f| %> + <%= f.error_notification %> + + <%= f.input :email, required: true, autofocus: true %> + +
+ <%= invisible_recaptcha_tags text: 'Register', class: 'button green' %> +
+ <% 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..2526976 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,27 @@ + + +
+
+
+

Returning User? Sign in

+
+
+ <%= simple_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "devise_page__form"}) 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)".html_safe %> + <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %> + +
+ <%= f.button :submit, "Sign In", class: "button green" %> +
+ <% 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..6f8d97f --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Sign In", new_session_path(resource_name) %> +<% end -%> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Register", 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..8faba67 --- /dev/null +++ b/app/views/email_template/edit.html.erb @@ -0,0 +1,36 @@ + + +
+
+
+

Email Template

+
+
+ <%= simple_form_for(@settings, url: email_template_path, html: { class:"devise_page__form" }) do |f| %> +
+ <%= f.input :send_secret_email_template, label: false, as: :trix_editor %> +
+ +
+ <%= f.button :submit, value: t('buttons.update'), class: "button green" %> +
+ <% end %> +
+
+ + + + +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b75556c..5522823 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,18 +1,21 @@ - + 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 %> + +
+ <%= yield %> +
<%= render partial: 'shared/footer' %> <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> diff --git a/app/views/pages/copyright.html.erb b/app/views/pages/copyright.html.erb index 583cd35..3eb26a9 100644 --- a/app/views/pages/copyright.html.erb +++ b/app/views/pages/copyright.html.erb @@ -1,6 +1,8 @@ -