diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee
index ba180df23..ae5d9664c 100644
--- a/app/assets/javascripts/app/controllers/_application_controller.coffee
+++ b/app/assets/javascripts/app/controllers/_application_controller.coffee
@@ -223,7 +223,7 @@ class App.Controller extends Spine.Controller
# remember requested url
if !checkOnly
location = window.location.hash
- if location isnt '#login' && location isnt '#logout' && location isnt '#keyboard_shortcuts'
+ if location && location isnt '#login' && location isnt '#logout' && location isnt '#keyboard_shortcuts'
@Config.set('requested_url', location)
return false if checkOnly
@@ -559,16 +559,20 @@ class App.Controller extends Spine.Controller
return if !@initLoadingDoneDelay
@clearDelay(@initLoadingDoneDelay)
+ renderScreenSuccess: (data) ->
+ App.TaskManager.touch(@task_key) if @task_key
+ @html App.view('generic/error/success')(data)
+
renderScreenError: (data) ->
- App.TaskManager.touch(@task_key)
+ App.TaskManager.touch(@task_key) if @task_key
@html App.view('generic/error/generic')(data)
renderScreenNotFound: (data) ->
- App.TaskManager.touch(@task_key)
+ App.TaskManager.touch(@task_key) if @task_key
@html App.view('generic/error/not_found')(data)
renderScreenUnauthorized: (data) ->
- App.TaskManager.touch(@task_key)
+ App.TaskManager.touch(@task_key) if @task_key
@html App.view('generic/error/unauthorized')(data)
locationVerify: (e) =>
diff --git a/app/assets/javascripts/app/controllers/default_route.coffee b/app/assets/javascripts/app/controllers/default_route.coffee
index bb27f5425..23d57eedc 100644
--- a/app/assets/javascripts/app/controllers/default_route.coffee
+++ b/app/assets/javascripts/app/controllers/default_route.coffee
@@ -28,5 +28,5 @@ class DefaultRouter extends App.Controller
@navigate '#dashboard', true
-App.Config.set( '', DefaultRouter, 'Routes' )
-App.Config.set( '/', DefaultRouter, 'Routes' )
+App.Config.set('', DefaultRouter, 'Routes')
+App.Config.set('/', DefaultRouter, 'Routes')
diff --git a/app/assets/javascripts/app/controllers/email_verify.coffee b/app/assets/javascripts/app/controllers/email_verify.coffee
new file mode 100644
index 000000000..4e8c60f6e
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/email_verify.coffee
@@ -0,0 +1,55 @@
+class Index extends App.Controller
+ constructor: ->
+ super
+ return if !@authenticate()
+ @verifyCall()
+
+ verifyCall: =>
+ @ajax(
+ id: 'email_verify'
+ type: 'POST'
+ url: @apiPath + '/users/email_verify'
+ data: JSON.stringify(token: @token)
+ processData: true
+ success: @success
+ error: @error
+ )
+
+ success: =>
+ new Success(el: @el)
+
+ error: =>
+ new Fail(el: @el)
+
+class Success extends App.ControllerContent
+ constructor: ->
+ super
+ @render()
+
+ # rerender view, e. g. on language change
+ @bind 'ui:rerender', =>
+ @render()
+
+ render: =>
+ @renderScreenSuccess(
+ detail: 'Woo hoo! Your email is verified!'
+ )
+ delay = =>
+ @navigate '#'
+ @delay(delay, 20500)
+
+class Fail extends App.ControllerContent
+ constructor: ->
+ super
+ @render()
+
+ # rerender view, e. g. on language change
+ @bind 'ui:rerender', =>
+ @render()
+
+ render: =>
+ @renderScreenError(
+ detail: 'Unable to verify email. Please contact your administrator.'
+ )
+
+App.Config.set('email_verify/:token', Index, 'Routes')
diff --git a/app/assets/javascripts/app/controllers/login.coffee b/app/assets/javascripts/app/controllers/login.coffee
index 106a12297..17a1aebc9 100644
--- a/app/assets/javascripts/app/controllers/login.coffee
+++ b/app/assets/javascripts/app/controllers/login.coffee
@@ -136,4 +136,4 @@ class Index extends App.ControllerContent
600
)
-App.Config.set( 'login', Index, 'Routes' )
+App.Config.set('login', Index, 'Routes')
diff --git a/app/assets/javascripts/app/controllers/password_reset.coffee b/app/assets/javascripts/app/controllers/password_reset.coffee
index 37cc4137c..8373c1d75 100644
--- a/app/assets/javascripts/app/controllers/password_reset.coffee
+++ b/app/assets/javascripts/app/controllers/password_reset.coffee
@@ -64,8 +64,8 @@ class Index extends App.ControllerContent
if data.token && @Config.get('developer_mode') is true
redirect = =>
@navigate "#password_reset_verify/#{data.token}"
- @delay( redirect, 2000 )
- @render( sent: true )
+ @delay(redirect, 2000)
+ @render(sent: true)
else
@$('[name=username]').val('')
@@ -75,7 +75,7 @@ class Index extends App.ControllerContent
)
@formEnable( @el.find('.form-password') )
-App.Config.set( 'password_reset', Index, 'Routes' )
+App.Config.set('password_reset', Index, 'Routes')
class Verify extends App.ControllerContent
events:
@@ -105,10 +105,10 @@ class Verify extends App.ControllerContent
url: @apiPath + '/users/password_reset_verify'
data: JSON.stringify(params)
processData: true
- success: @render_change
+ success: @renderChange
)
- render_change: (data) =>
+ renderChange: (data) =>
if data.message is 'ok'
configure_attributes = [
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 100, null: false, class: 'input', },
@@ -161,10 +161,10 @@ class Verify extends App.ControllerContent
url: @apiPath + '/users/password_reset_verify'
data: JSON.stringify(params)
processData: true
- success: @render_changed
+ success: @renderChanged
)
- render_changed: (data, status, xhr) =>
+ renderChanged: (data, status, xhr) =>
if data.message is 'ok'
App.Auth.login(
data:
diff --git a/app/assets/javascripts/app/controllers/widget/translation_inline.coffee b/app/assets/javascripts/app/controllers/widget/translation_inline.coffee
index ffca5ff16..ac5faa1bb 100644
--- a/app/assets/javascripts/app/controllers/widget/translation_inline.coffee
+++ b/app/assets/javascripts/app/controllers/widget/translation_inline.coffee
@@ -49,6 +49,7 @@ class Widget extends App.Controller
.on 'blur.translation', '.translation', (e) ->
element = $(e.target)
source = element.attr('title')
+ return if !source
# get new translation
translation_new = element.text()
@@ -62,6 +63,7 @@ class Widget extends App.Controller
App.i18n.setMap(source, translation_new)
# replace rest in page
+ source = source.replace('\'', '\\\'')
$(".translation[title='#{source}']").text(translation_new)
# update permanent translation mapString
diff --git a/app/assets/javascripts/app/controllers/widget/user_signup_check.coffee b/app/assets/javascripts/app/controllers/widget/user_signup_check.coffee
new file mode 100644
index 000000000..8d2e9c3c9
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/widget/user_signup_check.coffee
@@ -0,0 +1,82 @@
+class Widget extends App.Controller
+ constructor: ->
+
+ # for browser test
+ App.Event.bind('user_signup_verify', (user) ->
+ new Modal(user: user)
+ 'user_signup_verify'
+ )
+
+ App.Event.bind('auth:login', (user) =>
+ return if !user
+ @verifyLater(user.id)
+ 'user_signup_verify'
+ )
+ currentUserId = App.Session.get('id')
+ return if !currentUserId
+ @verifyLater(currentUserId)
+
+ verifyLater: (userId) =>
+ delay = =>
+ @verify(userId)
+ @delay(delay, 5000, 'user_signup_verify_dialog')
+
+ verify: (userId) ->
+ return if !userId
+ user = App.User.find(userId)
+ return if user.source isnt 'signup'
+ return if user.verified is true
+ currentTime = new Date().getTime()
+ createdAt = Date.parse(user.created_at)
+ diff = currentTime - createdAt
+ max = 1000 * 60 * 30 # show message if account is older then 30 minutes
+ return if diff < max
+ new Modal(user: user)
+
+class Modal extends App.ControllerModal
+ backdrop: false
+ keyboard: false
+ head: 'Account not verified'
+ small: true
+ buttonClose: false
+ buttonCancel: false
+ buttonSubmit: 'Resend verification email'
+
+ constructor: ->
+ super
+
+ content: =>
+ if !@sent
+ return App.i18n.translateContent('Your account is not verified. Please click the link in the verification email.')
+ content = App.i18n.translateContent('We\'ve sent an email to _%s_. Click the link in the email to verify your account.', @user.email)
+ content += '
'
+ content += App.i18n.translateContent('If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.')
+ content
+
+ onSubmit: =>
+ @ajax(
+ id: 'email_verify_send'
+ type: 'POST'
+ url: @apiPath + '/users/email_verify_send'
+ data: JSON.stringify(email: @user.email)
+ processData: true
+ success: @success
+ error: @error
+ )
+
+ success: (data) =>
+ @sent = true
+ @update()
+
+ # if in developer mode, redirect to verify
+ if data.token && @Config.get('developer_mode') is true
+ redirect = =>
+ @close()
+ @navigate "#email_verify/#{data.token}"
+ App.Delay.set(redirect, 4000)
+
+ error: =>
+ @contentInline = App.i18n.translateContent('Unable to send verify email.')
+ @update()
+
+App.Config.set('user_signup', Widget, 'Widgets')
diff --git a/app/assets/javascripts/app/views/generic/error/generic.jst.eco b/app/assets/javascripts/app/views/generic/error/generic.jst.eco
index 1a97ed816..8d44428f9 100644
--- a/app/assets/javascripts/app/views/generic/error/generic.jst.eco
+++ b/app/assets/javascripts/app/views/generic/error/generic.jst.eco
@@ -1,4 +1,4 @@
<%- @Icon('diagonal-cross', 'icon-error') %>
-
<% if @status isnt undefined: %><%- @T('Status Code') %>: <%= @status %>. <% end %><%= @detail %>
+ <% if @status isnt undefined: %><%- @T('Status Code') %>: <%= @status %>. <% end %><%- @T(@detail) %>
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/generic/error/success.jst.eco b/app/assets/javascripts/app/views/generic/error/success.jst.eco
new file mode 100644
index 000000000..61375596b
--- /dev/null
+++ b/app/assets/javascripts/app/views/generic/error/success.jst.eco
@@ -0,0 +1,4 @@
+
+ <%- @Icon('checkmark') %>
+
<%- @T(@detail) %>
+
\ No newline at end of file
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a2fb6c755..fbb091a6d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -73,6 +73,10 @@ class UsersController < ApplicationController
# if it's a signup, add user to customer role
if !current_user
+ if !params[:signup]
+ render json: { error_human: 'Only signup is possible!' }, status: :unprocessable_entity
+ return
+ end
user.updated_by_id = 1
user.created_by_id = 1
@@ -100,6 +104,9 @@ class UsersController < ApplicationController
user.role_ids = role_ids
user.group_ids = group_ids
+ # remember source (in case show email verify banner)
+ user.source = 'signup'
+
# else do assignment as defined
else
@@ -150,14 +157,11 @@ class UsersController < ApplicationController
# send email verify
if params[:signup] && !current_user
- token = Token.create(action: 'EmailVerify', user_id: user.id)
+ result = User.signup_new_token(user)
NotificationFactory::Mailer.notification(
template: 'signup',
user: user,
- objects: {
- token: token,
- user: user,
- }
+ objects: result
)
end
user_new = User.find(user.id).attributes_with_associations
@@ -393,6 +397,106 @@ class UsersController < ApplicationController
=begin
+Resource:
+POST /api/v1/users/email_verify
+
+Payload:
+{
+ "token": "SoMeToKeN",
+}
+
+Response:
+{
+ :message => 'ok'
+}
+
+Test:
+curl http://localhost/api/v1/users/email_verify.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"token": "SoMeToKeN"}'
+
+=end
+
+ def email_verify
+ if !params[:token]
+ render json: { message: 'No token!' }, status: :unprocessable_entity
+ return
+ end
+
+ user = User.signup_verify_via_token(params[:token], current_user)
+ if !user
+ render json: { message: 'Invalid token!' }, status: :unprocessable_entity
+ return
+ end
+
+ render json: { message: 'ok', user_email: user.email }, status: :ok
+ end
+
+=begin
+
+Resource:
+POST /api/v1/users/email_verify_send
+
+Payload:
+{
+ "email": "some_email@example.com"
+}
+
+Response:
+{
+ :message => 'ok'
+}
+
+Test:
+curl http://localhost/api/v1/users/email_verify_send.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"email": "some_email@example.com"}'
+
+=end
+
+ def email_verify_send
+
+ if !params[:email]
+ render json: { message: 'No email!' }, status: :unprocessable_entity
+ return
+ end
+
+ # check is verify is possible to send
+ user = User.find_by(email: params[:email].downcase)
+ if !user
+ render json: { error_human: 'No such user!' }, status: :unprocessable_entity
+ return
+ end
+
+ #if user.verified == true
+ # render json: { error_human: 'Already verified!' }, status: :unprocessable_entity
+ # return
+ #end
+
+ token = Token.create(action: 'Signup', user_id: user.id)
+
+ result = User.signup_new_token(user)
+ if result && result[:token]
+ user = result[:user]
+ NotificationFactory::Mailer.notification(
+ template: 'signup',
+ user: user,
+ objects: result
+ )
+
+ # only if system is in develop mode, send token back to browser for browser tests
+ if Setting.get('developer_mode') == true
+ render json: { message: 'ok', token: result[:token].name }, status: :ok
+ return
+ end
+
+ # token sent to user, send ok to browser
+ render json: { message: 'ok' }, status: :ok
+ return
+ end
+
+ # unable to generate token
+ render json: { message: 'failed' }, status: :ok
+ end
+
+=begin
+
Resource:
POST /api/v1/users/password_reset
diff --git a/app/models/token.rb b/app/models/token.rb
index 646b278be..625dbaf96 100644
--- a/app/models/token.rb
+++ b/app/models/token.rb
@@ -8,7 +8,7 @@ class Token < ActiveRecord::Base
create new token
- token = Token.create( action: 'PasswordReset', user_id: user.id )
+ token = Token.create(action: 'PasswordReset', user_id: user.id)
returns
@@ -34,7 +34,7 @@ returns
check token
- user = Token.check( action: 'PasswordReset', name: 'TheTokenItSelf' )
+ user = Token.check(action: 'PasswordReset', name: 'TheTokenItSelf')
returns
@@ -42,10 +42,10 @@ returns
=end
- def self.check( data )
+ def self.check(data)
# fetch token
- token = Token.find_by( action: data[:action], name: data[:name] )
+ token = Token.find_by(action: data[:action], name: data[:name])
return if !token
# check if token is still valid
@@ -81,8 +81,7 @@ cleanup old token
loop do
self.name = SecureRandom.hex(30)
-
- break if !Token.exists?( name: name )
+ break if !Token.exists?(name: name)
end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 34fed0745..347d73e64 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -375,7 +375,7 @@ returns
=begin
-reset reset password with token and set new password
+reset password with token and set new password
result = User.password_reset_via_token(token,password)
@@ -423,6 +423,63 @@ returns
=begin
+generate new token for signup
+
+ result = User.signup_new_token(user) # or email
+
+returns
+
+ result = {
+ token: token,
+ user: user,
+ }
+
+=end
+
+ def self.signup_new_token(user)
+ return if !user
+ return if !user.email
+
+ # generate token
+ token = Token.create(action: 'Signup', user_id: user.id)
+
+ {
+ token: token,
+ user: user,
+ }
+ end
+
+=begin
+
+verify signup with token
+
+ result = User.signup_verify_via_token(token, user)
+
+returns
+
+ result = user_model # user_model if token was verified
+
+=end
+
+ def self.signup_verify_via_token(token, user = nil)
+
+ # check token
+ local_user = Token.check(action: 'Signup', name: token)
+ return if !local_user
+
+ # if requested user is different to current user
+ return if user && local_user.id != user.id
+
+ # set verified
+ local_user.update_attributes(verified: true)
+
+ # delete token
+ Token.find_by(action: 'Signup', name: token).destroy
+ local_user
+ end
+
+=begin
+
merge two users to one
user = User.find(123)
diff --git a/config/routes/user.rb b/config/routes/user.rb
index fe4eacb89..429c798a7 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -22,4 +22,7 @@ Zammad::Application.routes.draw do
match api_path + '/users/:id', to: 'users#update', via: :put
match api_path + '/users/image/:hash', to: 'users#image', via: :get
+ match api_path + '/users/email_verify', to: 'users#email_verify', via: :post
+ match api_path + '/users/email_verify_send', to: 'users#email_verify_send', via: :post
+
end
diff --git a/test/browser/signup_password_change_and_reset_test.rb b/test/browser/signup_password_change_and_reset_test.rb
index 68d884f33..eda23c9f4 100644
--- a/test/browser/signup_password_change_and_reset_test.rb
+++ b/test/browser/signup_password_change_and_reset_test.rb
@@ -3,11 +3,11 @@ require 'browser_test_helper'
class SignupPasswordChangeAndResetTest < TestCase
def test_signup
- signup_user_email = 'signup-test-' + rand(999_999).to_s + '@example.com'
+ signup_user_email = "signup-test-#{rand(999_999)}@example.com"
@browser = browser_instance
- location( url: browser_url )
- click( css: 'a[href="#signup"]' )
- exists( css: '.signup' )
+ location(url: browser_url)
+ click(css: 'a[href="#signup"]')
+ exists(css: '.signup')
# signup
set(
@@ -30,10 +30,10 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_confirm"]',
value: 'some-pass',
)
- click( css: 'button.js-submit' )
+ click(css: 'button.js-submit')
sleep 5
- exists_not( css: '.signup' )
+ exists_not(css: '.signup')
match(
css: '.user-menu .user a',
@@ -41,11 +41,56 @@ class SignupPasswordChangeAndResetTest < TestCase
attribute: 'title',
)
+ # check email verify
+ location(url: "#{browser_url}#email_verify/not_existing")
+ watch_for(
+ css: '#content',
+ value: 'Unable to verify email',
+ )
+ logout()
+
+ login(
+ username: signup_user_email,
+ password: 'some-pass',
+ url: "#{browser_url}#email_verify/not_existing2",
+ )
+ watch_for(
+ css: '#content',
+ value: 'Unable to verify email',
+ )
+ execute(
+ js: 'App.Event.trigger("user_signup_verify", App.Session.get())',
+ )
+ watch_for(
+ css: '.modal',
+ value: 'Account not verified',
+ )
+ click(css: '.modal .js-submit')
+ execute(
+ js: 'App.Auth.logout()',
+ )
+ sleep 6
+ watch_for(
+ css: '#login',
+ )
+ login(
+ username: signup_user_email,
+ password: 'some-pass',
+ )
+ watch_for(
+ css: '#content',
+ value: 'Your email is verified',
+ )
+ exists_not(
+ css: '.modal',
+ )
+ sleep 2
+
# change password
- click( css: '.navbar-items-personal .user a' )
+ click(css: '.navbar-items-personal .user a')
sleep 1
- click( css: 'a[href="#profile"]' )
- click( css: 'a[href="#profile/password"]' )
+ click(css: 'a[href="#profile"]')
+ click(css: 'a[href="#profile/password"]')
set(
css: 'input[name="password_old"]',
value: 'nonexisiting',
@@ -58,7 +103,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_new_confirm"]',
value: 'some',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
@@ -73,7 +118,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_new_confirm"]',
value: 'some2',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'passwords do not match',
@@ -87,7 +132,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_new_confirm"]',
value: 'some',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
@@ -102,7 +147,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_new_confirm"]',
value: 'some-pass-new',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
@@ -117,7 +162,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_new_confirm"]',
value: 'some-pass-new2',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
@@ -133,7 +178,7 @@ class SignupPasswordChangeAndResetTest < TestCase
logout()
# reset password (not possible)
- location( url: browser_url + '/#password_reset_verify/not_existing_token' )
+ location(url: browser_url + '/#password_reset_verify/not_existing_token')
watch_for(
css: 'body',
@@ -147,7 +192,7 @@ class SignupPasswordChangeAndResetTest < TestCase
url: browser_url,
)
- location( url: browser_url + '/#password_reset' )
+ location(url: browser_url + '/#password_reset')
sleep 1
match_not(
@@ -157,13 +202,13 @@ class SignupPasswordChangeAndResetTest < TestCase
logout()
# reset password (correct way)
- click( css: 'a[href="#password_reset"]' )
+ click(css: 'a[href="#password_reset"]')
set(
css: 'input[name="username"]',
value: 'nonexisiting',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'address invalid',
@@ -173,7 +218,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="username"]',
value: signup_user_email,
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'sent password reset instructions',
@@ -194,7 +239,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_confirm"]',
value: 'some2',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'passwords do not match',
@@ -208,7 +253,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_confirm"]',
value: 'some',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'it must be at least',
@@ -222,7 +267,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_confirm"]',
value: 'some-pass-new',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'must contain at least 1 digit',
@@ -236,7 +281,7 @@ class SignupPasswordChangeAndResetTest < TestCase
css: 'input[name="password_confirm"]',
value: 'some-pass-new2',
)
- click( css: '.content .btn--primary' )
+ click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'Your password has been changed',