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
diff --git a/Gemfile b/Gemfile
index b003fde5..431d6c0a 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'
@@ -91,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 09d294a4..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)
@@ -151,6 +160,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 +231,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)
@@ -252,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)
@@ -445,6 +460,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 +530,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
@@ -526,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)
@@ -546,6 +578,7 @@ DEPENDENCIES
byebug
c_geohash
cane
+ capybara
countries
dalli
date_validator
@@ -561,6 +594,7 @@ DEPENDENCIES
guard-rspec
image_processing
jbuilder
+ jquery-rails
kaminari
listen
mailgun_rails
@@ -594,6 +628,7 @@ DEPENDENCIES
rspec-rails
rubocop
rufus-scheduler
+ sass-rails
sentry-rails
sentry-ruby
sentry-sidekiq
@@ -608,6 +643,8 @@ DEPENDENCIES
stamp
timecop
treetop
+ turbolinks
+ uglifier
vcr
versionist!
webmock
@@ -620,4 +657,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 00000000..c72f519d
Binary files /dev/null and b/app/assets/images/sck.png differ
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..80ea755d
--- /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 new_ui_session_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/shared_controller_methods.rb b/app/controllers/shared_controller_methods.rb
new file mode 100644
index 00000000..1aa48725
--- /dev/null
+++ b/app/controllers/shared_controller_methods.rb
@@ -0,0 +1,29 @@
+module SharedControllerMethods
+
+ include Pundit::Authorization
+
+ 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..0ac4236a
--- /dev/null
+++ b/app/controllers/ui/application_controller.rb
@@ -0,0 +1,6 @@
+module Ui
+ class ApplicationController < ActionController::Base
+ layout "application"
+ include SharedControllerMethods
+ end
+end
diff --git a/app/controllers/ui/sessions_controller.rb b/app/controllers/ui/sessions_controller.rb
new file mode 100644
index 00000000..56a0095a
--- /dev/null
+++ b/app/controllers/ui/sessions_controller.rb
@@ -0,0 +1,80 @@
+module Ui
+ class SessionsController < ApplicationController
+ include SharedControllerMethods
+ require 'uri'
+ require 'net/http'
+ require 'net/https'
+
+ def new
+ redirect_to ui_users_url if current_user
+ end
+
+ def index
+ redirect_to new_ui_session_path
+ end
+
+ def create
+ if params[:send_password_email]
+ reset_password_email
+ redirect_to new_ui_session_path
+ return
+ end
+
+ 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 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
+ flash.now.alert = "Email or password is invalid"
+ render "new"
+ end
+ end
+
+ def reset_password_email
+ user = User.where("lower(email) = lower(?) OR lower(username) = lower(?)",
+ params[:username_or_email], params[:username_or_email]).first
+
+ if user
+ authorize user, :request_password_reset?
+ user.send_password_reset
+ end
+ flash[:notice] = 'Please check your email to reset the password.'
+ end
+
+ def password_reset_landing
+ @token = params[:token]
+ end
+
+ def change_password
+ @token = params.require(:token)
+
+ if params.require(:password) != params.require(:password_confirmation)
+ flash[:alert] ="Your password doesn't match the confirmation"
+ render "password_reset_landing"
+ return
+ end
+
+ @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
+
+ 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..3ff935e2
--- /dev/null
+++ b/app/controllers/ui/users_controller.rb
@@ -0,0 +1,7 @@
+module Ui
+ class UsersController < ApplicationController
+ include SharedControllerMethods
+ def index
+ end
+ end
+end
diff --git a/app/controllers/v0/application_controller.rb b/app/controllers/v0/application_controller.rb
index 4498e915..8be8590b 100644
--- a/app/controllers/v0/application_controller.rb
+++ b/app/controllers/v0/application_controller.rb
@@ -8,13 +8,14 @@ class ApplicationController < ActionController::API
include ActionController::ImplicitRender
include ActionController::Caching
- include Pundit::Authorization
include PrettyJSON
include ErrorHandlers
helper ::UserHelper
include ::UserHelper
+ include SharedControllerMethods
+
respond_to :json
before_action :prepend_view_paths
@@ -66,26 +67,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/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/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/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/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 %>
+
+
+
+
+ <%= yield %>
+
+
+ <%= VERSION_FILE %>
+
+
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..ae746bc0
--- /dev/null
+++ b/app/views/ui/sessions/password_reset_landing.html.erb
@@ -0,0 +1,22 @@
+Change password
+
+<%= form_tag ui_change_password_path do %>
+
+
+
+
+ <%= hidden_field_tag :token, @token %>
+
+
+ <%= label_tag :password %>
+ <%= password_field_tag :password %>
+
+
+ <%= label_tag :password_confirmation, "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/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
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/config/application.rb b/config/application.rb
index caab0290..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'
@@ -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/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 d1664c88..e84ea258 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -7,6 +7,20 @@
end
mount Sidekiq::Web, at: "/sidekiq"
+ get "discourse/sso" => 'discourse#sso'
+
+ 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
# devices
resources :devices do
diff --git a/public/favicon.ico b/public/favicon.ico
index e69de29b..f36a475b 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/scripts/nginx-conf/api.smartcitizen.me.conf b/scripts/nginx-conf/api.smartcitizen.me.conf
index db548b7e..158d14f9 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.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 {
@@ -76,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|favicon.ico) {
+ expires max;
+ add_header Cache-Control public;
+ break;
+ }
try_files $uri/index.html $uri @app;
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
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)