From a134508c5d5c90f4493e71cc1449e8f41f8a44ba Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Fri, 18 Oct 2024 09:20:57 +0200 Subject: [PATCH 01/10] Monolith: ensure session based authentication works --- app/controllers/shared_controller_methods.rb | 27 ++++++++++++++++++++ app/controllers/ui/application_controller.rb | 5 ++++ app/controllers/ui/login_test_controller.rb | 6 +++++ app/controllers/v0/application_controller.rb | 22 ++-------------- app/controllers/v0/sessions_controller.rb | 2 ++ app/views/ui/login_test/show.html.erb | 13 ++++++++++ config/application.rb | 2 +- config/routes.rb | 4 +++ 8 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 app/controllers/shared_controller_methods.rb create mode 100644 app/controllers/ui/application_controller.rb create mode 100644 app/controllers/ui/login_test_controller.rb create mode 100644 app/views/ui/login_test/show.html.erb diff --git a/app/controllers/shared_controller_methods.rb b/app/controllers/shared_controller_methods.rb new file mode 100644 index 00000000..0447151e --- /dev/null +++ b/app/controllers/shared_controller_methods.rb @@ -0,0 +1,27 @@ +module SharedControllerMethods + + def self.included(klass) + klass.helper_method :current_user + end + + def current_user(fail_unauthorized=true) + if @current_user.nil? + if session[:user_id] + @current_user = User.find(session[:user_id]) + elsif doorkeeper_token + # return render text: 'abc' + @current_user = User.find(doorkeeper_token.resource_owner_id) + elsif ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) # username and password + authenticate_with_http_basic do |username, password| + if user = User.find_by(username: username) and user.authenticate_with_legacy_support(password) + @current_user = user + elsif fail_unauthorized + self.headers["WWW-Authenticate"] = %(Basic realm="Application", Token realm="Application") + raise Smartcitizen::Unauthorized.new "Invalid Username/Password Combination" + end + end + end + end + @current_user + end +end diff --git a/app/controllers/ui/application_controller.rb b/app/controllers/ui/application_controller.rb new file mode 100644 index 00000000..fa45dc65 --- /dev/null +++ b/app/controllers/ui/application_controller.rb @@ -0,0 +1,5 @@ +module Ui + class ApplicationController < ActionController::Base + include SharedControllerMethods + end +end diff --git a/app/controllers/ui/login_test_controller.rb b/app/controllers/ui/login_test_controller.rb new file mode 100644 index 00000000..3d59aa8b --- /dev/null +++ b/app/controllers/ui/login_test_controller.rb @@ -0,0 +1,6 @@ +module Ui + class LoginTestController < ApplicationController + def show + end + end +end diff --git a/app/controllers/v0/application_controller.rb b/app/controllers/v0/application_controller.rb index 4498e915..62496fe5 100644 --- a/app/controllers/v0/application_controller.rb +++ b/app/controllers/v0/application_controller.rb @@ -15,6 +15,8 @@ class ApplicationController < ActionController::API helper ::UserHelper include ::UserHelper + include SharedControllerMethods + respond_to :json before_action :prepend_view_paths @@ -66,26 +68,6 @@ def set_rate_limit_whitelist end end - def current_user(fail_unauthorized=true) - if @current_user.nil? - if doorkeeper_token - # return render text: 'abc' - @current_user = User.find(doorkeeper_token.resource_owner_id) - elsif ActionController::HttpAuthentication::Basic.has_basic_credentials?(request) # username and password - authenticate_with_http_basic do |username, password| - if user = User.find_by(username: username) and user.authenticate_with_legacy_support(password) - @current_user = user - elsif fail_unauthorized - self.headers["WWW-Authenticate"] = %(Basic realm="Application", Token realm="Application") - raise Smartcitizen::Unauthorized.new "Invalid Username/Password Combination" - end - end - end - end - @current_user - end - helper_method :current_user - def check_if_authorized! if current_user.nil? if params[:access_token] diff --git a/app/controllers/v0/sessions_controller.rb b/app/controllers/v0/sessions_controller.rb index f47de389..bc7d2ea9 100644 --- a/app/controllers/v0/sessions_controller.rb +++ b/app/controllers/v0/sessions_controller.rb @@ -8,8 +8,10 @@ def create authorize user, :show? if user && user.authenticate_with_legacy_support(params[:password]) # $analytics.track("login:successful", user.id) + session[:user_id] = user.id render json: { access_token: user.access_token!.token }, status: :ok else + session[:user_id] = nil raise Smartcitizen::UnprocessableEntity.new({ message: {password: 'is incorrect'}, # to be removed password: 'is incorrect' # to replace the above diff --git a/app/views/ui/login_test/show.html.erb b/app/views/ui/login_test/show.html.erb new file mode 100644 index 00000000..09a3a082 --- /dev/null +++ b/app/views/ui/login_test/show.html.erb @@ -0,0 +1,13 @@ +

Current user is: <%= current_user %>

+

Cookies:

+ diff --git a/config/application.rb b/config/application.rb index caab0290..c7ff4651 100644 --- a/config/application.rb +++ b/config/application.rb @@ -71,7 +71,7 @@ class Application < Rails::Application request_specs: false end - config.api_only = true + config.api_only = false config.active_storage.service_urls_expire_in = 30.minutes config.middleware.use Rack::Deflater end diff --git a/config/routes.rb b/config/routes.rb index d1664c88..4f291d5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,10 @@ end mount Sidekiq::Web, at: "/sidekiq" + namespace "ui" do + get "login_test", to: "login_test#show" + end + api_version(module: "V0", path: {value: "v0"}, header: {name: "Accept", value: "application/vnd.smartcitizen; version=0"}, default: true, defaults: { format: :json }) do # devices resources :devices do From efecc1022990f4f48655e7bcc658520c9194f636 Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Wed, 23 Oct 2024 16:54:17 +0200 Subject: [PATCH 02/10] Merge authentication code and discourse landing callback from auth app --- Gemfile | 5 + Gemfile.lock | 26 +++- app/assets/config/manifest.js | 3 + app/assets/images/sck.png | Bin 0 -> 715 bytes app/assets/javascripts/application.js | 16 +++ app/assets/stylesheets/application.css | 15 +++ app/assets/stylesheets/sessions.scss | 63 ++++++++++ app/controllers/application_controller.rb | 4 +- app/controllers/discourse_controller.rb | 32 +++++ app/controllers/ui/application_controller.rb | 1 + app/controllers/ui/login_test_controller.rb | 6 - app/controllers/ui/sessions_controller.rb | 119 ++++++++++++++++++ app/controllers/ui/users_controller.rb | 10 ++ app/views/layouts/application.html.erb | 25 ++++ app/views/ui/login_test/show.html.erb | 13 -- app/views/ui/sessions/new.html.erb | 31 +++++ .../sessions/password_reset_landing.html.erb | 22 ++++ app/views/ui/users/index.html.erb | 25 ++++ app/views/ui/users/show.html.erb | 2 + config/application.rb | 2 +- config/environments/development.rb | 4 + config/environments/production.rb | 4 + config/initializers/assets.rb | 11 ++ config/routes.rb | 11 +- scripts/nginx-conf/api.smartcitizen.me.conf | 49 +++----- 25 files changed, 441 insertions(+), 58 deletions(-) create mode 100644 app/assets/config/manifest.js create mode 100644 app/assets/images/sck.png create mode 100644 app/assets/javascripts/application.js create mode 100644 app/assets/stylesheets/application.css create mode 100644 app/assets/stylesheets/sessions.scss create mode 100644 app/controllers/discourse_controller.rb delete mode 100644 app/controllers/ui/login_test_controller.rb create mode 100644 app/controllers/ui/sessions_controller.rb create mode 100644 app/controllers/ui/users_controller.rb create mode 100644 app/views/layouts/application.html.erb delete mode 100644 app/views/ui/login_test/show.html.erb create mode 100644 app/views/ui/sessions/new.html.erb create mode 100644 app/views/ui/sessions/password_reset_landing.html.erb create mode 100644 app/views/ui/users/index.html.erb create mode 100644 app/views/ui/users/show.html.erb create mode 100644 config/initializers/assets.rb diff --git a/Gemfile b/Gemfile index b003fde5..43ecca27 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,11 @@ gem 'rails', '6.1.7.3' gem 'sidekiq', '~> 6' gem 'doorkeeper', '~> 5' +gem 'jquery-rails' +gem 'sass-rails' +gem 'turbolinks' +gem 'uglifier' + # To resize active storage images: # Revise if this is needed after Rails 6.0 gem 'image_processing' diff --git a/Gemfile.lock b/Gemfile.lock index 09d294a4..53b56266 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,6 +151,7 @@ GEM tzinfo eventmachine (1.2.7) excon (0.99.0) + execjs (2.9.1) factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) @@ -221,6 +222,10 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) + jquery-rails (4.6.0) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) json (2.6.3) jwt (2.7.0) kaminari (1.2.2) @@ -445,6 +450,16 @@ GEM rufus-scheduler (3.8.2) fugit (~> 1.1, >= 1.1.6) safe_yaml (1.0.5) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt sentry-rails (5.9.0) railties (>= 5.0) sentry-ruby (~> 5.9.0) @@ -505,8 +520,13 @@ GEM timeout (0.3.2) treetop (1.6.12) polyglot (~> 0.3) + turbolinks (5.2.1) + turbolinks-source (~> 5.2) + turbolinks-source (5.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + uglifier (4.2.1) + execjs (>= 0.3.0, < 3) unaccent (0.4.0) unf (0.1.4) unf_ext @@ -561,6 +581,7 @@ DEPENDENCIES guard-rspec image_processing jbuilder + jquery-rails kaminari listen mailgun_rails @@ -594,6 +615,7 @@ DEPENDENCIES rspec-rails rubocop rufus-scheduler + sass-rails sentry-rails sentry-ruby sentry-sidekiq @@ -608,6 +630,8 @@ DEPENDENCIES stamp timecop treetop + turbolinks + uglifier vcr versionist! webmock @@ -620,4 +644,4 @@ RUBY VERSION ruby 3.0.6p216 BUNDLED WITH - 2.5.21 + 2.5.22 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 00000000..b16e53d6 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/sck.png b/app/assets/images/sck.png new file mode 100644 index 0000000000000000000000000000000000000000..c72f519d8c373f7d119514645c5fe76f7424054d GIT binary patch literal 715 zcmeAS@N?(olHy`uVBq!ia0vp^`ao>L!3-pIUram(q(TCGLR|m<|8IKeDV)T^fGC42 zz{-UPei7g84>Vt@B*-tAK|nx3!N9>GAR(ckp<%*=1sgUTIB?;?gAX63EjU`tz`(@g z>EamTaeQjn?WWZVJWE23DsS2(_Dbph|M~2PbytMWYS-H(?4jnYq*$H&MEmtyrCItP zu2=;-?`^&(vu0)%|3kAGQW?DaHlEa4l(qaR_n!s2n|`vL<=SF;{o&=4)5>CZnB}cI z%2U5Qd&kF&o4a%ltYuqwE9B9)NyYrn9~PcF?Zej9@ARhasnBZMm2gcdy7f#VJY8*f)*e@z?ov)ysH%Y#6?13s-qA|GVZ~ zoq%Ip;~o#Q6^AC;uG21SP>OoBIrh@GcMIhov>yJq@6LJUKi+K(T)BoFuF+RZUjI^h zvUgqV9%;LGx$d_Y|80IFTc`45>c?Jxo*QTO9&nk)U%|xlvxIxzUIQCB=_yw~hisD< zh|`tZa_ZH`2MM$Ke9pJnfN^>hUaeV*) literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 00000000..e07c5a83 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,16 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require turbolinks +//= require_tree . diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 00000000..f9cd5b34 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any styles + * defined in the other CSS/SCSS files in this directory. It is generally better to create a new + * file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/app/assets/stylesheets/sessions.scss b/app/assets/stylesheets/sessions.scss new file mode 100644 index 00000000..a050bd3e --- /dev/null +++ b/app/assets/stylesheets/sessions.scss @@ -0,0 +1,63 @@ +// Place all the styles related to the sessions controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ +$blue: #0C2EFB; +$black: #212121; +$grey: #E3E3E3; +$red: #FA5161; + +@import url('https://fonts.googleapis.com/css?family=Roboto+Condensed:400,300,300italic,400italic,700,700italic|Roboto:400,700,700italic,400italic'); +@import url('https://fonts.googleapis.com/css?family=Kanit:400,500,600,700,900'); + +*{ + font-family:'Kanit'; +} +body{ + background-color: #e3e3e3; +} +h1{ + font-weight:700 !important; +} +.button { + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 18px; +} +.bg-blue{ + background-color: $blue !important; +} + +.button:hover{ + cursor: pointer; + color: white; + text-decoration:underline; +} + +.bg-red{ + background-color: $red; +} +.color-red{ + color: $red; +} + +input[type=text],input[type=password] { + padding:7px; + border:none; + border-bottom:2px solid #BEBEBE; +} + +#flash_notice{ + padding:20px; + color: white; + background-color: $blue; +} + +#flash_alert{ + padding:20px; + color: white; + background-color: $red; +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d5ef819..6eae9fce 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,3 @@ # this file is required for errbit notifier -class ApplicationController < ActionController::API - include ActionController::ImplicitRender - include ActionController::Helpers +class ApplicationController < ActionController::Base end diff --git a/app/controllers/discourse_controller.rb b/app/controllers/discourse_controller.rb new file mode 100644 index 00000000..f643fbef --- /dev/null +++ b/app/controllers/discourse_controller.rb @@ -0,0 +1,32 @@ +class DiscourseController < ApplicationController + include SharedControllerMethods + + DISCOURSE_SSO_SECRET = ENV.fetch("discourse_sso_secret") + DISCOURSE_ENDPOINT = ENV.fetch("discourse_endpoint") + def sso + if !current_user + session[:discourse_url] = request.url + redirect_to root_path(goto: request.path), notice: 'Please Log In before using SSO' + return + end + secret = DISCOURSE_SSO_SECRET + sso = SingleSignOn.parse(request.query_string, secret) + sso.email = current_user.email # from devise + #sso.name = current_user.full_name # this is a custom method on the User class + sso.username = current_user.email # from devise + #sso.username = current_user.username + sso.external_id = current_user.id # from devise + sso.sso_secret = secret + + redirect_to sso.to_url("#{DISCOURSE_ENDPOINT}session/sso_login") + rescue => e + Rails.logger.error(e.message) + Rails.logger.error(e.backtrace) + #flash[:error] = 'SSO error' + render inline: "Error, check logs" + + #redirect_to "/" + #redirect_to root + end + +end diff --git a/app/controllers/ui/application_controller.rb b/app/controllers/ui/application_controller.rb index fa45dc65..0ac4236a 100644 --- a/app/controllers/ui/application_controller.rb +++ b/app/controllers/ui/application_controller.rb @@ -1,5 +1,6 @@ module Ui class ApplicationController < ActionController::Base + layout "application" include SharedControllerMethods end end diff --git a/app/controllers/ui/login_test_controller.rb b/app/controllers/ui/login_test_controller.rb deleted file mode 100644 index 3d59aa8b..00000000 --- a/app/controllers/ui/login_test_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Ui - class LoginTestController < ApplicationController - def show - end - end -end diff --git a/app/controllers/ui/sessions_controller.rb b/app/controllers/ui/sessions_controller.rb new file mode 100644 index 00000000..5bf2d650 --- /dev/null +++ b/app/controllers/ui/sessions_controller.rb @@ -0,0 +1,119 @@ +module Ui + class SessionsController < ApplicationController + include SharedControllerMethods + require 'uri' + require 'net/http' + require 'net/https' + + def new + redirect_to users_url if current_user + end + + def index + # If you click 'Sign in to your account' and it fails with a + # 'Email or password invalid' and you then refresh the page + # it would end here with a rails error + redirect_to '/' + end + + def create + if params[:send_password_email] + logger.warn '---- send_password_email' + reset_password_email + redirect_to new_ui_session_path + return + end + + if params[:change_password] + logger.warn '---- change_password' + change_password + #redirect_to '/' + #return + end + + logger.warn '---- Create - normal login' + user = User.where("lower(email) = lower(?) OR lower(username) = lower(?)", + params[:username_or_email], params[:username_or_email]).first + if user && user.authenticate_with_legacy_support(params[:password]) + logger.warn '---- with_legacy' + session[:user_id] = user.id + + # If we came from Discourse (with the goto param), redirect to /discourse/sso + if params[:goto].include? 'discourse' + redirect_to session[:discourse_url] + else + logger.warn '---- without discourse' + redirect_to (session[:user_return_to] || ui_users_path), notice: "You have been successfully logged in!" + end + else + flash.now.alert = "Email or password is invalid" + render "new" + end + end + + def reset_password_email + uri = URI.parse("https://api.smartcitizen.me/v0/password_resets") + https = Net::HTTP.new(uri.host,uri.port) + https.use_ssl = true + + req = Net::HTTP::Post.new(uri.path) + req.body = URI.encode_www_form({:email_or_username => params[:username_or_email] }) + res = https.request(req) + jsonres = JSON.parse( res.body ) + + flash[:alert] = jsonres["message"] + + if flash[:alert].include? "Delivered" + flash[:notice] = 'Please check your email to reset the password.' + else + flash[:notice] = 'Is your username / email correct?' + end + + end + + def password_reset_landing + # Landing page from the email + logger.warn '---- password_reset (landing page from email)' + token = params[:token] + logger.warn @token + end + + def change_password + logger.warn '---- Send PATCH request with token + password' + logger.warn params[:token] + + #curl -XPATCH "https://api.smartcitizen.me/v0/password_resets/kP3LH5G7J6k9rqjVmGwYOA?password=12341234" + uri = URI.parse("https://api.smartcitizen.me/v0/password_resets/#{params[:token]}") + https = Net::HTTP.new(uri.host,uri.port) + https.use_ssl = true + + req = Net::HTTP::Patch.new(uri.path) + req.body = URI.encode_www_form({:password => params[:password] }) + res = https.request(req) + jsonres = JSON.parse( res.body ) + + logger.warn jsonres + + if jsonres["message"] + flash[:alert] = jsonres["message"] + if flash[:alert].include? "Could" + flash[:notice] = 'Your reset code might be too old or have been used before.' + end + end + + if jsonres["username"] + flash[:notice] = 'Changed password for: ' + flash[:alert] = jsonres["username"] + end + + redirect_to new_ui_session_path + + end + + def destroy + session[:user_id] = nil + redirect_to login_url, notice: "Logged out!" + end + +end +end diff --git a/app/controllers/ui/users_controller.rb b/app/controllers/ui/users_controller.rb new file mode 100644 index 00000000..d80815a3 --- /dev/null +++ b/app/controllers/ui/users_controller.rb @@ -0,0 +1,10 @@ +module Ui + class UsersController < ApplicationController + include SharedControllerMethods + def index + end + + def show + end + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 00000000..0a2bdfca --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,25 @@ + + + + SmartCitizen Authentication helper + + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + <%= csrf_meta_tags %> + + + + <% flash.each do |name, msg| %> + <%= content_tag :div, msg, id: "flash_#{name}" %> + <% end %> + +
+
+ SCK logo + <%= yield %> +
+
+ <%= VERSION_FILE %> + + diff --git a/app/views/ui/login_test/show.html.erb b/app/views/ui/login_test/show.html.erb deleted file mode 100644 index 09a3a082..00000000 --- a/app/views/ui/login_test/show.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -

Current user is: <%= current_user %>

-

Cookies:

-
    - <% cookies.each do |key, value|%> -
  • <%= key %>: <%= value %>
  • - <% end %> -
    - - - - -
    -
diff --git a/app/views/ui/sessions/new.html.erb b/app/views/ui/sessions/new.html.erb new file mode 100644 index 00000000..41521e95 --- /dev/null +++ b/app/views/ui/sessions/new.html.erb @@ -0,0 +1,31 @@ +

WELCOME!

+ +<%= form_tag ui_sessions_path do %> +
+ +
+
+ <%= label_tag :username_or_email, "Username or Email" %>
+ <%= text_field_tag :username_or_email, params[:username_or_email], autofocus: params[:username_or_email].blank? %> +
+
+ <%= label_tag :password %>
+ <%= password_field_tag :password %> +
+
+ <%= hidden_field_tag :goto, params[:goto] %> + <%= submit_tag "SIGN IN TO YOUR ACCOUNT", class: 'button bg-blue', style: 'width:100%' %> +
+ +
+
Don't have an account?
+ SIGN UP +
+ +
+
+
Forgot your password?
+
+ <%= submit_tag "RESET PASSWORD", name: 'send_password_email', class: 'button bg-red' %> +
+<% end %> diff --git a/app/views/ui/sessions/password_reset_landing.html.erb b/app/views/ui/sessions/password_reset_landing.html.erb new file mode 100644 index 00000000..ca018432 --- /dev/null +++ b/app/views/ui/sessions/password_reset_landing.html.erb @@ -0,0 +1,22 @@ +

Change password

+ +<%= form_tag change_password_path do %> +
+ + +
+ <%= hidden_field_tag :token, params[:token] %> +
+
+ <%= label_tag :password %>
+ <%= password_field_tag :password %> +
+
+ <%= label_tag :password, "Confirm new password" %>
+ <%= password_field_tag :password_confirmation %> +
+
+ <%= submit_tag "Change my password", name: 'change_password', class: 'button bg-red', style: 'width:100%' %> +
+
+<% end %> diff --git a/app/views/ui/users/index.html.erb b/app/views/ui/users/index.html.erb new file mode 100644 index 00000000..fceae1b9 --- /dev/null +++ b/app/views/ui/users/index.html.erb @@ -0,0 +1,25 @@ +

User info

+ +
+
+ + <%= link_to "Smartcitizen Website", "https://www.smartcitizen.me/" %>
+ <%= link_to "Smartcitizen Forum", "https://forum.smartcitizen.me/" %>
+ <%= link_to "Smartcitizen API", "https://api.smartcitizen.me/" %>
+ <%= link_to "Smartcitizen Developer documentation", "https://developer.smartcitizen.me/" %>
+ <%= link_to "See JSON user data from example.smartcitizen.me", "http://example.smartcitizen.me" %> + +
+ + <% if current_user %> +

Logged in as <%= current_user.email %>.

+

Your access token: +
<%= current_user.access_token.token %>

+

<%= link_to "Log Out", logout_path, class: 'button bg-red' %>

+ <% else %> +

Not logged in!

+ GO BACK + <% end %> + +
+
diff --git a/app/views/ui/users/show.html.erb b/app/views/ui/users/show.html.erb new file mode 100644 index 00000000..740f7da6 --- /dev/null +++ b/app/views/ui/users/show.html.erb @@ -0,0 +1,2 @@ +

Nothing here.

+<%= link_to "See my info on /users", ui_users_url %> diff --git a/config/application.rb b/config/application.rb index c7ff4651..046f3c21 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ require "action_controller/railtie" require "action_mailer/railtie" require "action_view/railtie" -#require "sprockets/railtie" +require "sprockets/railtie" # require "rails/test_unit/railtie" # require 'actionpack/action_caching' diff --git a/config/environments/development.rb b/config/environments/development.rb index a23c9e15..4aeb069d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -27,6 +27,10 @@ config.cache_store = :null_store end + config.assets.debug = true + config.assets.digest = true + config.assets.raise_ruuntime_errors = true + # Store uploaded files on the local file system (see config/storage.yml for options). #config.active_storage.service = :amazon config.active_storage.service = :local diff --git a/config/environments/production.rb b/config/environments/production.rb index 6071dd7b..f7bc7f05 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,6 +29,10 @@ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + config.assets.js_compressor = :uglifier + config.assets.compile = false + config.assets.digest = true + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :amazon diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 00000000..01ef3e66 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/config/routes.rb b/config/routes.rb index 4f291d5b..c77e64a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,8 +7,17 @@ end mount Sidekiq::Web, at: "/sidekiq" + get "discourse/sso" => 'discourse#sso' + + get 'login', to: 'ui/sessions#new', as: 'login' + get 'logout', to: 'ui/sessions#destroy', as: 'logout' + get 'password_reset/:token', to: 'ui/sessions#password_reset_landing', as: 'password_reset' + + post 'change_password', to: 'ui/sessions#change_password', as: 'change_password' + namespace "ui" do - get "login_test", to: "login_test#show" + resources :users, as: "users" + resources :sessions, as: "sessions" end api_version(module: "V0", path: {value: "v0"}, header: {name: "Accept", value: "application/vnd.smartcitizen; version=0"}, default: true, defaults: { format: :json }) do diff --git a/scripts/nginx-conf/api.smartcitizen.me.conf b/scripts/nginx-conf/api.smartcitizen.me.conf index db548b7e..75e95dca 100644 --- a/scripts/nginx-conf/api.smartcitizen.me.conf +++ b/scripts/nginx-conf/api.smartcitizen.me.conf @@ -18,7 +18,7 @@ server { } server { - server_name id.smartcitizen.me id2.smartcitizen.me staging-id.smartcitizen.me; + server_name id.smartcitizen.me; listen 80; listen [::]:80; @@ -28,42 +28,25 @@ server { ssl_certificate /etc/ssl/star_smartcitizen_me.pem; ssl_certificate_key /etc/ssl/star_smartcitizen_me.key; - try_files $uri/index.html $uri @app; - - location @app { - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Ssl on; # Optional - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_pass http://auth:3000; + rewrite ^/users/(.*)$ https://api.smartcitizen.com/ui/users/$1 permanent; + rewrite ^/sessions/(.*)$ https://api.smartcitizen.com/ui/sessions/$1 permanent; + rewrite ^/(.*)$ https://api.smartcitizen.com/$1 permanent; +} - # Reverse proxy cache - #proxy_cache smartcitizen; - proxy_cache_lock on; - proxy_cache_use_stale updating; +server { + server_name staging-id.smartcitizen.me; - # Add CORS - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'Authorization,OnboardingSession,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + listen 80; + listen [::]:80; - # Tell client that this pre-flight info is valid for 30 days - add_header 'Access-Control-Max-Age' 2592000; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - add_header 'Content-Length' 0; - return 200; # Well, maybe 204, no content. - } - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Credentials' 'true' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Authorization,OnboardingSession,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always; - } + listen 443 ssl; + listen [::]:443 ssl; + ssl_certificate /etc/ssl/star_smartcitizen_me.pem; + ssl_certificate_key /etc/ssl/star_smartcitizen_me.key; - client_max_body_size 4G; - keepalive_timeout 10; + rewrite ^/users/(.*)$ https://staging-api.smartcitizen.com/ui/users/$1 permanent; + rewrite ^/sessions/(.*)$ https://staging-api.smartcitizen.com/ui/sessions/$1 permanent; + rewrite ^/(.*)$ https://staging-api.smartcitizen.com/$1 permanent; } server { From f9537e4fb7967a0a33a8758134b218703f20bc4f Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Sun, 20 Oct 2024 14:37:18 +0200 Subject: [PATCH 03/10] refactor login UI a bit --- app/controllers/discourse_controller.rb | 2 +- app/controllers/shared_controller_methods.rb | 2 + app/controllers/ui/sessions_controller.rb | 133 +++++++----------- app/controllers/ui/users_controller.rb | 3 - app/controllers/v0/application_controller.rb | 1 - .../v0/password_resets_controller.rb | 2 - app/policies/user_policy.rb | 3 +- .../sessions/password_reset_landing.html.erb | 6 +- app/views/ui/users/show.html.erb | 2 - config/routes.rb | 11 +- spec/policies/user_policy_spec.rb | 4 +- spec/requests/v0/password_resets_spec.rb | 1 + 12 files changed, 63 insertions(+), 107 deletions(-) delete mode 100644 app/views/ui/users/show.html.erb diff --git a/app/controllers/discourse_controller.rb b/app/controllers/discourse_controller.rb index f643fbef..80ea755d 100644 --- a/app/controllers/discourse_controller.rb +++ b/app/controllers/discourse_controller.rb @@ -6,7 +6,7 @@ class DiscourseController < ApplicationController def sso if !current_user session[:discourse_url] = request.url - redirect_to root_path(goto: request.path), notice: 'Please Log In before using SSO' + redirect_to new_ui_session_path(goto: request.path), notice: 'Please Log In before using SSO' return end secret = DISCOURSE_SSO_SECRET diff --git a/app/controllers/shared_controller_methods.rb b/app/controllers/shared_controller_methods.rb index 0447151e..1aa48725 100644 --- a/app/controllers/shared_controller_methods.rb +++ b/app/controllers/shared_controller_methods.rb @@ -1,5 +1,7 @@ module SharedControllerMethods + include Pundit::Authorization + def self.included(klass) klass.helper_method :current_user end diff --git a/app/controllers/ui/sessions_controller.rb b/app/controllers/ui/sessions_controller.rb index 5bf2d650..56a0095a 100644 --- a/app/controllers/ui/sessions_controller.rb +++ b/app/controllers/ui/sessions_controller.rb @@ -6,114 +6,75 @@ class SessionsController < ApplicationController require 'net/https' def new - redirect_to users_url if current_user + redirect_to ui_users_url if current_user end def index - # If you click 'Sign in to your account' and it fails with a - # 'Email or password invalid' and you then refresh the page - # it would end here with a rails error - redirect_to '/' - end - - def create - if params[:send_password_email] - logger.warn '---- send_password_email' - reset_password_email redirect_to new_ui_session_path - return end - if params[:change_password] - logger.warn '---- change_password' - change_password - #redirect_to '/' - #return - end + def create + if params[:send_password_email] + reset_password_email + redirect_to new_ui_session_path + return + end - logger.warn '---- Create - normal login' - user = User.where("lower(email) = lower(?) OR lower(username) = lower(?)", - params[:username_or_email], params[:username_or_email]).first - if user && user.authenticate_with_legacy_support(params[:password]) - logger.warn '---- with_legacy' - session[:user_id] = user.id + user = User.where("lower(email) = lower(?) OR lower(username) = lower(?)", + params[:username_or_email], params[:username_or_email]).first + if user && user.authenticate_with_legacy_support(params[:password]) + session[:user_id] = user.id - # If we came from Discourse (with the goto param), redirect to /discourse/sso - if params[:goto].include? 'discourse' - redirect_to session[:discourse_url] + if params[:goto].include? 'discourse' + redirect_to session[:discourse_url] + else + redirect_to (session[:user_return_to] || ui_users_path), notice: "You have been successfully logged in!" + end else - logger.warn '---- without discourse' - redirect_to (session[:user_return_to] || ui_users_path), notice: "You have been successfully logged in!" + flash.now.alert = "Email or password is invalid" + render "new" end - else - flash.now.alert = "Email or password is invalid" - render "new" end - end - - def reset_password_email - uri = URI.parse("https://api.smartcitizen.me/v0/password_resets") - https = Net::HTTP.new(uri.host,uri.port) - https.use_ssl = true - req = Net::HTTP::Post.new(uri.path) - req.body = URI.encode_www_form({:email_or_username => params[:username_or_email] }) - res = https.request(req) - jsonres = JSON.parse( res.body ) + def reset_password_email + user = User.where("lower(email) = lower(?) OR lower(username) = lower(?)", + params[:username_or_email], params[:username_or_email]).first - flash[:alert] = jsonres["message"] - - if flash[:alert].include? "Delivered" + if user + authorize user, :request_password_reset? + user.send_password_reset + end flash[:notice] = 'Please check your email to reset the password.' - else - flash[:notice] = 'Is your username / email correct?' end - end - - def password_reset_landing - # Landing page from the email - logger.warn '---- password_reset (landing page from email)' - token = params[:token] - logger.warn @token - end - - def change_password - logger.warn '---- Send PATCH request with token + password' - logger.warn params[:token] - - #curl -XPATCH "https://api.smartcitizen.me/v0/password_resets/kP3LH5G7J6k9rqjVmGwYOA?password=12341234" - uri = URI.parse("https://api.smartcitizen.me/v0/password_resets/#{params[:token]}") - https = Net::HTTP.new(uri.host,uri.port) - https.use_ssl = true + def password_reset_landing + @token = params[:token] + end - req = Net::HTTP::Patch.new(uri.path) - req.body = URI.encode_www_form({:password => params[:password] }) - res = https.request(req) - jsonres = JSON.parse( res.body ) + def change_password + @token = params.require(:token) - logger.warn jsonres + if params.require(:password) != params.require(:password_confirmation) + flash[:alert] ="Your password doesn't match the confirmation" + render "password_reset_landing" + return + end - if jsonres["message"] - flash[:alert] = jsonres["message"] - if flash[:alert].include? "Could" - flash[:notice] = 'Your reset code might be too old or have been used before.' + @user = User.find_by(password_reset_token: @token) + if @user + authorize @user, :update_password? + @user.update({ password: params.require(:password), password_reset_token: nil }) + flash[:notice] = "Changed password for: #{@user.username}" + redirect_to new_ui_session_path + else + flash[:alert] = 'Your reset code might be too old or have been used before.' + render "password_reset_landing" end end - if jsonres["username"] - flash[:notice] = 'Changed password for: ' - flash[:alert] = jsonres["username"] + def destroy + session[:user_id] = nil + redirect_to login_url, notice: "Logged out!" end - - redirect_to new_ui_session_path - - end - - def destroy - session[:user_id] = nil - redirect_to login_url, notice: "Logged out!" end - -end end diff --git a/app/controllers/ui/users_controller.rb b/app/controllers/ui/users_controller.rb index d80815a3..3ff935e2 100644 --- a/app/controllers/ui/users_controller.rb +++ b/app/controllers/ui/users_controller.rb @@ -3,8 +3,5 @@ class UsersController < ApplicationController include SharedControllerMethods def index end - - def show - end end end diff --git a/app/controllers/v0/application_controller.rb b/app/controllers/v0/application_controller.rb index 62496fe5..8be8590b 100644 --- a/app/controllers/v0/application_controller.rb +++ b/app/controllers/v0/application_controller.rb @@ -8,7 +8,6 @@ class ApplicationController < ActionController::API include ActionController::ImplicitRender include ActionController::Caching - include Pundit::Authorization include PrettyJSON include ErrorHandlers diff --git a/app/controllers/v0/password_resets_controller.rb b/app/controllers/v0/password_resets_controller.rb index e260b054..7bdd09dd 100644 --- a/app/controllers/v0/password_resets_controller.rb +++ b/app/controllers/v0/password_resets_controller.rb @@ -28,7 +28,6 @@ def create # 2/3 - The associated user object is returned, indicating a valid token def show @user = User.find_by!(password_reset_token: params[:id]) - @current_user = @user authorize @user, :update_password? render 'users/show', status: :ok end @@ -36,7 +35,6 @@ def show # 3/3 - The password reset is submitted and committed to the database def update @user = User.find_by!(password_reset_token: params[:id]) - @current_user = @user authorize @user, :update_password? if @user.update({ password: params.require(:password), password_reset_token: nil }) render 'users/show', status: :ok diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 92b64eb9..63ba866c 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -21,7 +21,6 @@ def request_password_reset? end def update_password? - update? + create? end - end diff --git a/app/views/ui/sessions/password_reset_landing.html.erb b/app/views/ui/sessions/password_reset_landing.html.erb index ca018432..ae746bc0 100644 --- a/app/views/ui/sessions/password_reset_landing.html.erb +++ b/app/views/ui/sessions/password_reset_landing.html.erb @@ -1,18 +1,18 @@

Change password

-<%= form_tag change_password_path do %> +<%= form_tag ui_change_password_path do %>
- <%= hidden_field_tag :token, params[:token] %> + <%= hidden_field_tag :token, @token %>
<%= label_tag :password %>
<%= password_field_tag :password %>
- <%= label_tag :password, "Confirm new password" %>
+ <%= label_tag :password_confirmation, "Confirm new password" %>
<%= password_field_tag :password_confirmation %>
diff --git a/app/views/ui/users/show.html.erb b/app/views/ui/users/show.html.erb deleted file mode 100644 index 740f7da6..00000000 --- a/app/views/ui/users/show.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -

Nothing here.

-<%= link_to "See my info on /users", ui_users_url %> diff --git a/config/routes.rb b/config/routes.rb index c77e64a5..e84ea258 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,15 +9,16 @@ get "discourse/sso" => 'discourse#sso' - get 'login', to: 'ui/sessions#new', as: 'login' - get 'logout', to: 'ui/sessions#destroy', as: 'logout' - get 'password_reset/:token', to: 'ui/sessions#password_reset_landing', as: 'password_reset' - - post 'change_password', to: 'ui/sessions#change_password', as: 'change_password' + get "login", to: redirect("/ui/sessions/new") + get "logout", to: redirect("/ui/sessions/destroy") + get "password_reset/:token", to: redirect("/ui/password_reset/%{token}") namespace "ui" do resources :users, as: "users" + get "sessions/destroy", to: "sessions#destroy" resources :sessions, as: "sessions" + post 'change_password', to: 'sessions#change_password', as: 'change_password' + get 'password_reset/:token', to: 'sessions#password_reset_landing', as: 'password_reset' end api_version(module: "V0", path: {value: "v0"}, header: {name: "Accept", value: "application/vnd.smartcitizen; version=0"}, default: true, defaults: { format: :json }) do diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 1d6d4de0..d542b2ab 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -12,7 +12,7 @@ it { is_expected.to_not permitz(:update) } it { is_expected.to_not permitz(:destroy) } it { is_expected.to permitz(:request_password_reset) } - it { is_expected.to_not permitz(:update_password) } + it { is_expected.to permitz(:update_password) } end context "for a user" do @@ -22,7 +22,7 @@ it { is_expected.to permitz(:update) } it { is_expected.to permitz(:destroy) } it { is_expected.to_not permitz(:request_password_reset) } - it { is_expected.to permitz(:update_password) } + it { is_expected.to_not permitz(:update_password) } end end diff --git a/spec/requests/v0/password_resets_spec.rb b/spec/requests/v0/password_resets_spec.rb index 1cfd239c..13c1b440 100644 --- a/spec/requests/v0/password_resets_spec.rb +++ b/spec/requests/v0/password_resets_spec.rb @@ -117,6 +117,7 @@ it "can reset password with valid token" do expect(user.authenticate('newpass')).to be_falsey j = api_put "password_resets/#{user.password_reset_token}", { password: 'newpass' } + p response expect(j["username"]).to eq(user.username) expect(response.status).to eq(200) From eb7fd57dc9bf9685f08e00ba85a0ce5f75375dc4 Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Sun, 20 Oct 2024 14:37:26 +0200 Subject: [PATCH 04/10] remove auth app from docker compose --- compose.override.local.yml | 2 -- compose.yml | 1 - compose/app.yml | 1 - compose/auth.yml | 9 --------- 4 files changed, 13 deletions(-) delete mode 100644 compose/auth.yml diff --git a/compose.override.local.yml b/compose.override.local.yml index 696dd628..269098cb 100644 --- a/compose.override.local.yml +++ b/compose.override.local.yml @@ -1,6 +1,4 @@ services: - auth: - restart: "no" app: build: args: !reset [] diff --git a/compose.yml b/compose.yml index 0b0428c2..2fb4790a 100644 --- a/compose.yml +++ b/compose.yml @@ -1,7 +1,6 @@ include: - compose/db.yml - compose/redis.yml - - compose/auth.yml - compose/app.yml - compose/sidekiq.yml - compose/mqtt-task.yml diff --git a/compose/app.yml b/compose/app.yml index 0d202996..d729e4d9 100644 --- a/compose/app.yml +++ b/compose/app.yml @@ -12,7 +12,6 @@ services: depends_on: # We disable some containers in production - db - - auth - redis - sidekiq - mqtt-task-main-1 diff --git a/compose/auth.yml b/compose/auth.yml deleted file mode 100644 index 39267ae6..00000000 --- a/compose/auth.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - auth: - env_file: ../.env - image: smartcitizen/auth - ports: - - "3001:3000" - restart: always - environment: - db_pool_size: 30 From 53bf9ea0df10b0c26564f3bc959347521e6f3d4e Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Sun, 20 Oct 2024 17:06:11 +0200 Subject: [PATCH 05/10] add acceptance tests for login and discourse --- Gemfile | 1 + Gemfile.lock | 13 +++ spec/features/session_management_spec.rb | 122 +++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 spec/features/session_management_spec.rb diff --git a/Gemfile b/Gemfile index 43ecca27..431d6c0a 100644 --- a/Gemfile +++ b/Gemfile @@ -96,6 +96,7 @@ group :development, :test do gem 'brakeman', github: 'presidentbeef/brakeman', require: false gem 'byebug' gem 'cane' + gem "capybara" gem 'factory_bot_rails' gem 'faker' gem 'guard-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index 53b56266..04ee3a7d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -119,6 +119,15 @@ GEM c_geohash (1.1.2) cane (3.0.0) parallel + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) choice (0.2.0) coderay (1.1.3) concurrent-ruby (1.2.2) @@ -257,6 +266,7 @@ GEM json (>= 1.7.7) rest-client (>= 1.6.7) marcel (1.0.2) + matrix (0.4.2) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) @@ -546,6 +556,8 @@ GEM workflow-activerecord (6.0.0) activerecord (>= 6.0) workflow (~> 3.0) + xpath (3.2.0) + nokogiri (~> 1.8) yard (0.9.34) zeitwerk (2.6.8) zonebie (0.6.1) @@ -566,6 +578,7 @@ DEPENDENCIES byebug c_geohash cane + capybara countries dalli date_validator diff --git a/spec/features/session_management_spec.rb b/spec/features/session_management_spec.rb new file mode 100644 index 00000000..2de4ba58 --- /dev/null +++ b/spec/features/session_management_spec.rb @@ -0,0 +1,122 @@ +require "rails_helper" + +feature "User logs in" do + + let(:password) { "password123" } + let(:user) { create(:user, password: password, password_confirmation: password) } + + scenario "user logs in with email" do + visit "/login" + fill_in "Username or Email", with: user.email + fill_in "Password", with: password + click_on "SIGN IN TO YOUR ACCOUNT" + expect(page).to have_current_path(ui_users_path) + expect(page).to have_content("You have been successfully logged in!") + end + + scenario "user logs in with username" do + visit "/login" + fill_in "Username or Email", with: user.username + fill_in "Password", with: password + click_on "SIGN IN TO YOUR ACCOUNT" + expect(page).to have_current_path(ui_users_path) + expect(page).to have_content("You have been successfully logged in!") + end + + scenario "user logs in with erroneous password" do + visit "/login" + fill_in "Username or Email", with: user.username + fill_in "Password", with: "notarealpassword" + click_on "SIGN IN TO YOUR ACCOUNT" + expect(page).to have_current_path(ui_sessions_path) + expect(page).to have_content("Email or password is invalid") + end + + scenario "user logs in with erroneous username" do + visit "/login" + fill_in "Username or Email", with: "notarealusername" + fill_in "Password", with: password + click_on "SIGN IN TO YOUR ACCOUNT" + expect(page).to have_current_path(ui_sessions_path) + expect(page).to have_content("Email or password is invalid") + end + + scenario "user logs out" do + visit "/login" + fill_in "Username or Email", with: user.email + fill_in "Password", with: password + click_on "SIGN IN TO YOUR ACCOUNT" + click_on "Log Out" + expect(page).to have_current_path(new_ui_session_path) + expect(page).to have_content("Logged out!") + end + + scenario "user resets email" do + visit "/login" + fill_in "Username or Email", with: user.email + click_on "RESET PASSWORD" + expect(page).to have_content("Please check your email to reset the password") + visit "/password_reset/#{user.reload.password_reset_token}" + fill_in "Password", with: "newpassword456" + fill_in "Confirm new password", with: "newpassword456" + click_on "Change my password" + expect(page).to have_content("Changed password for: #{user.username}") + fill_in "Username or Email", with: user.username + fill_in "Password", with: "newpassword456" + click_on "SIGN IN TO YOUR ACCOUNT" + expect(page).to have_current_path(ui_users_path) + expect(page).to have_content("You have been successfully logged in!") + end + + scenario "user resets email but gives incorrect password confirmation" do + visit "/login" + fill_in "Username or Email", with: user.email + click_on "RESET PASSWORD" + expect(page).to have_content("Please check your email to reset the password") + visit "/password_reset/#{user.reload.password_reset_token}" + fill_in "Password", with: "newpassword456" + fill_in "Confirm new password", with: "password456" + click_on "Change my password" + expect(page).to have_current_path(ui_change_password_path) + expect(page).to have_content("Your password doesn't match the confirmation") + end + + scenario "user attempts to reset password with an erroneous token" do + visit "/password_reset/notarealtoken" + fill_in "Password", with: "newpassword456" + fill_in "Confirm new password", with: "newpassword456" + click_on "Change my password" + expect(page).to have_current_path(ui_change_password_path) + expect(page).to have_content("Your reset code might be too old or have been used before.") + end + + scenario "discourse_login_flow" do + secret = DiscourseController::DISCOURSE_SSO_SECRET + nonce = "12345" + return_sso_url = "https://discourse.example.com" + unencrypted_payload = "nonce=#{nonce}&return_sso_url=#{return_sso_url}" + payload = Base64.encode64(unencrypted_payload) + signature = OpenSSL::HMAC.hexdigest("sha256", secret, payload) + visit "/discourse/sso?sso=#{CGI.escape(payload)}&sig=#{signature}" + expect(page).to have_current_path(new_ui_session_path + "?goto=%2Fdiscourse%2Fsso") + expect(page).to have_content("Please Log In before using SSO") + fill_in "Username or Email", with: user.email + fill_in "Password", with: password + begin + click_on "SIGN IN TO YOUR ACCOUNT" + # The next doesn't exist, we just want to check the URL is correct: + rescue ActionController::RoutingError + expect(page.current_url).to match(/^#{DiscourseController::DISCOURSE_ENDPOINT}/) + uri = URI.parse(page.current_url) + params = CGI.parse(uri.query) + check_signature = OpenSSL::HMAC.hexdigest("sha256", secret, params["sso"][0]) + expect(params["sig"][0]).to eq(check_signature) + payload = CGI.parse(Base64.decode64(CGI.unescape(params["sso"][0]))) + expect(payload["nonce"][0]).to eq(nonce) + expect(payload["username"][0]).to eq(user.email) + expect(payload["email"][0]).to eq(user.email) + expect(payload["external_id"][0]).to eq(user.id.to_s) + expect(payload["return_sso_url"][0]).to eq(return_sso_url) + end + end +end From 23d04dff947a2730661145f4e75f62359a6d0d60 Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Sun, 20 Oct 2024 17:18:03 +0200 Subject: [PATCH 06/10] add stubbed discourse secrets in test runner --- .github/workflows/ruby.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index ca3d5387..24fd6f3c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -56,6 +56,8 @@ jobs: TEST_DB_USERNAME: postgres TEST_DB_PASSWORD: postgres TEST_DB_NAME: postgres + discourse_sso_secret: "secret" + discourse_endpoint: "https://example.com" PG_PORT: ${{ job.services.postgres.ports['5432'] }} #- name: Deploy to server via SSH From 1a2499dff286b764322b1f59be3d7ce6ff409146 Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Sun, 20 Oct 2024 17:28:27 +0200 Subject: [PATCH 07/10] fix nginx redirects --- scripts/nginx-conf/api.smartcitizen.me.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/nginx-conf/api.smartcitizen.me.conf b/scripts/nginx-conf/api.smartcitizen.me.conf index 75e95dca..88962260 100644 --- a/scripts/nginx-conf/api.smartcitizen.me.conf +++ b/scripts/nginx-conf/api.smartcitizen.me.conf @@ -44,9 +44,9 @@ server { ssl_certificate /etc/ssl/star_smartcitizen_me.pem; ssl_certificate_key /etc/ssl/star_smartcitizen_me.key; - rewrite ^/users/(.*)$ https://staging-api.smartcitizen.com/ui/users/$1 permanent; - rewrite ^/sessions/(.*)$ https://staging-api.smartcitizen.com/ui/sessions/$1 permanent; - rewrite ^/(.*)$ https://staging-api.smartcitizen.com/$1 permanent; + rewrite ^/users/(.*)$ https://staging-api.smartcitizen.me/ui/users/$1 permanent; + rewrite ^/sessions/(.*)$ https://staging-api.smartcitizen.me/ui/sessions/$1 permanent; + rewrite ^/(.*)$ https://staging-api.smartcitizen.me/$1 permanent; } server { From 970130ebad88ac1c42ecfe0e19dc533ede4cd9f2 Mon Sep 17 00:00:00 2001 From: Oscar Gonzalez Date: Sun, 20 Oct 2024 15:49:48 +0000 Subject: [PATCH 08/10] NGINX setup to serve assets --- compose/web.yml | 1 + scripts/nginx-conf/api.smartcitizen.me.conf | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/compose/web.yml b/compose/web.yml index 550f1960..a3857300 100644 --- a/compose/web.yml +++ b/compose/web.yml @@ -11,6 +11,7 @@ services: - 443:443 - 443:443/udp volumes: + - ../public:/app/public - ../scripts/nginx-conf/api.smartcitizen.me.conf:/etc/nginx/conf.d/api.smartcitizen.me.conf - ../scripts/nginx.conf:/etc/nginx/nginx.conf - ../scripts/certs:/etc/ssl:ro diff --git a/scripts/nginx-conf/api.smartcitizen.me.conf b/scripts/nginx-conf/api.smartcitizen.me.conf index 88962260..355627d8 100644 --- a/scripts/nginx-conf/api.smartcitizen.me.conf +++ b/scripts/nginx-conf/api.smartcitizen.me.conf @@ -59,6 +59,13 @@ server { listen [::]:443 ssl; ssl_certificate /etc/ssl/star_smartcitizen_me.pem; ssl_certificate_key /etc/ssl/star_smartcitizen_me.key; + root /app/public; + + location ~* ^/assets { + expires max; + add_header Cache-Control public; + break; + } try_files $uri/index.html $uri @app; From 35d71fff8902595cc8f3fb28a9e36fd95efeb82a Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Sun, 20 Oct 2024 17:57:23 +0200 Subject: [PATCH 09/10] add favicon --- public/favicon.ico | Bin 0 -> 3096 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/favicon.ico b/public/favicon.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f36a475bfe050ea0ae924e2c1d87205b1283aabc 100644 GIT binary patch literal 3096 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 mfD&+os^>Ok@` Date: Sun, 20 Oct 2024 18:42:26 +0200 Subject: [PATCH 10/10] nginx rule for favicon --- scripts/nginx-conf/api.smartcitizen.me.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nginx-conf/api.smartcitizen.me.conf b/scripts/nginx-conf/api.smartcitizen.me.conf index 355627d8..158d14f9 100644 --- a/scripts/nginx-conf/api.smartcitizen.me.conf +++ b/scripts/nginx-conf/api.smartcitizen.me.conf @@ -61,7 +61,7 @@ server { ssl_certificate_key /etc/ssl/star_smartcitizen_me.key; root /app/public; - location ~* ^/assets { + location ~* ^/(assets|favicon.ico) { expires max; add_header Cache-Control public; break;