Fixed login, new_password and invitation views.

This commit is contained in:
benni 2012-10-08 11:51:56 +02:00
parent f20ae890dd
commit 1708df3f6c
18 changed files with 200 additions and 122 deletions

View File

@ -10,8 +10,10 @@ class InvitesController < ApplicationController
def create def create
@invite = Invite.new(params[:invite]) @invite = Invite.new(params[:invite])
if @invite.save if @invite.save
Mailer.delay.invite(FoodsoftConfig.scope, @invite.id)
redirect_to back_or_default_path, notice: "Benutzerin wurde erfolgreich eingeladen." redirect_to back_or_default_path, notice: "Benutzerin wurde erfolgreich eingeladen."
else else
logger.debug "[debug] errors: #{@invite.errors.messages}"
render action: :new render action: :new
end end
end end

View File

@ -1,7 +1,7 @@
# encoding: utf-8 # encoding: utf-8
class LoginController < ApplicationController class LoginController < ApplicationController
skip_before_filter :authenticate # no authentication since this is the login page skip_before_filter :authenticate # no authentication since this is the login page
before_filter :validate_token, :only => [:password, :update_password] before_filter :validate_token, :only => [:new_password, :update_password]
# Display the form to enter an email address requesting a token to set a new password. # Display the form to enter an email address requesting a token to set a new password.
def forgot_password def forgot_password
@ -14,7 +14,7 @@ class LoginController < ApplicationController
user.reset_password_token = user.new_random_password(16) user.reset_password_token = user.new_random_password(16)
user.reset_password_expires = Time.now.advance(:days => 2) user.reset_password_expires = Time.now.advance(:days => 2)
if user.save if user.save
email = Mailer.reset_password(user).deliver Mailer.delay.reset_password(FoodsoftConfig.scope, user.id)
logger.debug("Sent password reset email to #{user.email}.") logger.debug("Sent password reset email to #{user.email}.")
end end
end end
@ -23,7 +23,7 @@ class LoginController < ApplicationController
# Set a new password with a token from the password reminder email. # Set a new password with a token from the password reminder email.
# Called with params :id => User.id and :token => User.reset_password_token to specify a new password. # Called with params :id => User.id and :token => User.reset_password_token to specify a new password.
def password def new_password
end end
# Sets a new password. # Sets a new password.
@ -36,13 +36,13 @@ class LoginController < ApplicationController
@user.save @user.save
redirect_to login_url, :notice => "Dein Passwort wurde aktualisiert. Du kannst Dich jetzt anmelden." redirect_to login_url, :notice => "Dein Passwort wurde aktualisiert. Du kannst Dich jetzt anmelden."
else else
render :action => 'password' render :new_password
end end
end end
# Invited users. # For invited users.
def invite def accept_invitation
@invite = Invite.find_by_token(params[:id]) @invite = Invite.find_by_token(params[:token])
if (@invite.nil? || @invite.expires_at < Time.now) if (@invite.nil? || @invite.expires_at < Time.now)
flash[:error] = "Deine Einladung ist nicht (mehr) gültig." flash[:error] = "Deine Einladung ist nicht (mehr) gültig."
render :action => 'login' render :action => 'login'
@ -71,7 +71,7 @@ class LoginController < ApplicationController
def validate_token def validate_token
@user = User.find_by_id_and_reset_password_token(params[:id], params[:token]) @user = User.find_by_id_and_reset_password_token(params[:id], params[:token])
if (@user.nil? || @user.reset_password_expires < Time.now) if (@user.nil? || @user.reset_password_expires < Time.now)
flash[:error] = "Ungültiger oder abgelaufener Token. Bitte versuch es erneut." flash.now.error = "Ungültiger oder abgelaufener Token. Bitte versuch es erneut."
render :action => 'forgot_password' render :action => 'forgot_password'
end end
end end

View File

@ -130,4 +130,17 @@ module ApplicationHelper
:target => "_blank" :target => "_blank"
end end
def bootstrap_flash
flash_messages = []
flash.each do |type, message|
type = :success if type == :notice
type = :error if type == :alert
text = content_tag(:div,
content_tag(:button, raw("&times;"), :class => "close", "data-dismiss" => "alert") +
message, :class => "alert fade in alert-#{type}")
flash_messages << text if message
end
flash_messages.join("\n").html_safe
end
end end

View File

@ -19,20 +19,22 @@ class Mailer < ActionMailer::Base
# Sends an email with instructions on how to reset the password. # Sends an email with instructions on how to reset the password.
# Assumes user.setResetPasswordToken has been successfully called already. # Assumes user.setResetPasswordToken has been successfully called already.
def reset_password(user) def reset_password(foodcoop, user_id)
@user = user set_foodcoop_scope(foodcoop)
@link = url_for(:controller => "login", :action => "password", :id => user.id, :token => user.reset_password_token) @user = User.find(user_id)
@link = new_password_url(id: @user.id, token: @user.reset_password_token)
mail :to => user.email, mail :to => @user.email,
:subject => "[#{FoodsoftConfig[:name]}] Neues Passwort für/ New password for #{user.nick}" :subject => "[#{FoodsoftConfig[:name]}] Neues Passwort für/ New password for #{@user.nick}"
end end
# Sends an invite email. # Sends an invite email.
def invite(invite) def invite(foodcoop, invite_id)
@invite = invite set_foodcoop_scope(foodcoop)
@link = url_for(:controller => "login", :action => "invite", :id => invite.token) @invite = Invite.find(invite_id)
@link = accept_invitation_url(token: @invite.token)
mail :to => invite.email, mail :to => @invite.email,
:subject => "Einladung in die Foodcoop #{FoodsoftConfig[:name]} - Invitation to the Foodcoop" :subject => "Einladung in die Foodcoop #{FoodsoftConfig[:name]} - Invitation to the Foodcoop"
end end
@ -81,5 +83,11 @@ class Mailer < ActionMailer::Base
mail :to => user.email, mail :to => user.email,
:subject => "[#{FoodsoftConfig[:name]}] \"#{task.name}\" braucht noch Leute!" :subject => "[#{FoodsoftConfig[:name]}] \"#{task.name}\" braucht noch Leute!"
end end
private
def set_foodcoop_scope(foodcoop)
ActionMailer::Base.default_url_options[:foodcoop] = foodcoop
end
end end

View File

@ -12,20 +12,16 @@ class Invite < ActiveRecord::Base
validates_presence_of :expires_at validates_presence_of :expires_at
validate :email_not_already_registered, :on => :create validate :email_not_already_registered, :on => :create
before_validation :set_token_and_expires_at
protected protected
# Before validation, set token and expires_at. # Before validation, set token and expires_at.
def before_validation def set_token_and_expires_at
self.token = Digest::SHA1.hexdigest(Time.now.to_s + rand(100).to_s) self.token = Digest::SHA1.hexdigest(Time.now.to_s + rand(100).to_s)
self.expires_at = Time.now.advance(:days => 7) self.expires_at = Time.now.advance(:days => 7)
end end
# Sends an email to the invited user.
def after_create
Mailer.invite(self).deliver
end
private private
# Custom validation: check that email does not already belong to a registered user. # Custom validation: check that email does not already belong to a registered user.

View File

@ -0,0 +1,29 @@
!!! 5
%html(lang="en")
%head
%meta(charset="utf-8")
%meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1")
%meta(name="viewport" content="width=device-width, initial-scale=1.0")
%title= content_for?(:title) ? yield(:title) : "Foodsoft"
= csrf_meta_tags
/ Le HTML5 shim, for IE6-8 support of HTML elements
/[if lt IE 9]
= javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js"
= stylesheet_link_tag "application", :media => "all"
%link(href="images/apple-touch-icon-144x144.png" rel="apple-touch-icon-precomposed" sizes="144x144")
%link(href="images/apple-touch-icon-114x114.png" rel="apple-touch-icon-precomposed" sizes="114x114")
%link(href="images/apple-touch-icon-72x72.png" rel="apple-touch-icon-precomposed" sizes="72x72")
%link(href="images/apple-touch-icon.png" rel="apple-touch-icon-precomposed")
//%link(href="images/favicon.ico" rel="shortcut icon")
= yield(:head)
%body
= yield
/
Javascripts
\==================================================
/ Placed at the end of the document so the pages load faster
= javascript_include_tag "application"
= yield(:javascript)

View File

@ -1,58 +1,35 @@
!!! 5 = render layout: 'layouts/header' do
%html(lang="en") .row-fluid
%head
%meta(charset="utf-8")
%meta(http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1")
%meta(name="viewport" content="width=device-width, initial-scale=1.0")
%title= content_for?(:title) ? yield(:title) : "Foodsoft"
= csrf_meta_tags
/ Le HTML5 shim, for IE6-8 support of HTML elements
/[if lt IE 9]
= javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js"
= stylesheet_link_tag "application", :media => "all"
%link(href="images/apple-touch-icon-144x144.png" rel="apple-touch-icon-precomposed" sizes="144x144")
%link(href="images/apple-touch-icon-114x114.png" rel="apple-touch-icon-precomposed" sizes="114x114")
%link(href="images/apple-touch-icon-72x72.png" rel="apple-touch-icon-precomposed" sizes="72x72")
%link(href="images/apple-touch-icon.png" rel="apple-touch-icon-precomposed")
%link(href="images/favicon.ico" rel="shortcut icon")
.navbar.navbar-fixed-top
.navbar-inner
.container-fluid
%a.btn.btn-navbar(data-target=".nav-collapse" data-toggle="collapse")
%span.icon-bar
%span.icon-bar
%span.icon-bar
= link_to 'Foodsoft', root_path(anchor: ''), class: 'brand'
.container.nav-collapse
= render_navigation expand_all: true, renderer: :bootstrap
%body .container-fluid
.row-fluid .row-fluid
- if content_for?(:sidebar)
.navbar.navbar-fixed-top .span2
.navbar-inner = yield(:sidebar)
.container-fluid .span10
%a.btn.btn-navbar(data-target=".nav-collapse" data-toggle="collapse") = bootstrap_flash
%span.icon-bar
%span.icon-bar
%span.icon-bar
= link_to 'Foodsoft', root_path(anchor: ''), class: 'brand'
.container.nav-collapse
= render_navigation expand_all: true, renderer: :bootstrap
.container-fluid
.row-fluid
- if content_for?(:sidebar)
.span2
= yield(:sidebar)
.span10
- if show_title?
.page-header
%h1= yield(:title)
= yield
- else
- if show_title? - if show_title?
.page-header .page-header
%h1= yield(:title) %h1= yield(:title)
= yield = yield
- else
%footer = bootstrap_flash
%p - if show_title?
Foodsoft, open source software to manage a non-profit food coop. .page-header
%h1= yield(:title)
= yield
/ %footer
Javascripts %p
\================================================== Foodsoft, open source software to manage a non-profit food coop.
/ Placed at the end of the document so the pages load faster
= javascript_include_tag "application"

View File

@ -1,19 +1,13 @@
!!! = render layout: 'layouts/header' do
%html .container
%head .row
%meta{"http-equiv" => "content-type", :content => "text/html;charset=UTF-8"} .span6.offset3
%title= "FoodSoft - " + (yield(:title) or controller.controller_name) = bootstrap_flash
= stylesheet_link_tag 'application' - if show_title?
= javascript_include_tag 'application' .page-header
= csrf_meta_tags %h1= yield(:title)
= yield(:head) = yield
%body
#login %footer
- if yield(:title) %p
%h1= yield(:title) Foodsoft, open source software to manage a non-profit food coop.
- flash.each do |name, msg|
= content_tag :div, msg, :class => "flash #{name}"
= yield
#meta
Foodcoop
= link_to_if FoodsoftConfig[:homepage], FoodsoftConfig[:name], FoodsoftConfig[:homepage]

View File

@ -1,3 +1,7 @@
- content_for :javascript do
:javascript
$('user_nick').focus();
- title "Einladung in die #{FoodsoftConfig[:name]}" - title "Einladung in die #{FoodsoftConfig[:name]}"
%p %p
Du bist eingeladen worden als Mitglied der Gruppe Du bist eingeladen worden als Mitglied der Gruppe
@ -12,7 +16,8 @@
Gründen, weitergeben. Du kannst auch entscheiden, wieviel deiner persönlichen Gründen, weitergeben. Du kannst auch entscheiden, wieviel deiner persönlichen
Daten für alle einsehbar sein sollen. 'Alle' bedeutet hier alle Foodcoop-Mitglieder. Daten für alle einsehbar sein sollen. 'Alle' bedeutet hier alle Foodcoop-Mitglieder.
Die Administratoren haben aber jederzeit Zugriff auf deine Daten. Die Administratoren haben aber jederzeit Zugriff auf deine Daten.
= simple_form_for @user, url: {action: 'invite'} do |form| = simple_form_for @user, url: accept_invitation_path do |form|
= render partial: 'shared/user_form_fields', locals: {f: form} = render partial: 'shared/user_form_fields', locals: {f: form}
= submit_tag "Absenden" .form-actions
= javascript_tag("$('user_nick').focus()") = submit_tag "Foodsoft Account erstellen", class: 'btn'

View File

@ -7,5 +7,6 @@
= simple_form_for User.new, url: {action: 'reset_password'} do |form| = simple_form_for User.new, url: {action: 'reset_password'} do |form|
= form.input :email = form.input :email
= form.submit "Neues Passwort anfordern" .form-actions
= link_to "Abbrechen", :back = form.submit "Neues Passwort anfordern", class: 'btn'
= link_to "oder abbrechen", :back

View File

@ -3,4 +3,5 @@
= simple_form_for @user, :url => {:action => 'update_password', :id => @user.id, :token => @user.reset_password_token} do |form| = simple_form_for @user, :url => {:action => 'update_password', :id => @user.id, :token => @user.reset_password_token} do |form|
= form.input :password = form.input :password
= form.input :password_confirmation = form.input :password_confirmation
= form.submit .form-actions
= form.submit 'Neues Passwort speichern', class: 'btn'

View File

@ -1,7 +1,7 @@
Hallo <%= @user.nick %>, Hallo <%= @user.nick %>,
du (oder jemand anderes) hat auf der FoodSoft-Website ein neues Passwort angefordert. du (oder jemand anderes) hat auf der FoodSoft-Website ein neues Passwort angefordert.
Um ein neues Passwort einzugeben, gehe zu: <%= @link %> Um ein neues Passwort einzugeben, gehe zu: <%= raw @link %>
Dieser Link kann nur einmal aufgerufen werden und läuft am <%= I18n.l @user.reset_password_expires %> ab. Dieser Link kann nur einmal aufgerufen werden und läuft am <%= I18n.l @user.reset_password_expires %> ab.
Wenn du das Passwort nicht ändern möchtest oder diese Email nicht ausgelöst hast, brauchst du nichts zu tun. Dein bisheriges Passwort wurde nicht geändert. Wenn du das Passwort nicht ändern möchtest oder diese Email nicht ausgelöst hast, brauchst du nichts zu tun. Dein bisheriges Passwort wurde nicht geändert.
@ -11,7 +11,7 @@ Grüße sendet die Foodsoft! :)
Hi <%= @user.nick %>, Hi <%= @user.nick %>,
you have (or someone else has) requested a new password. you have (or someone else has) requested a new password.
In order to choose a new password follow this link: <%= @link %> In order to choose a new password follow this link: <%= raw @link %>
This link works only once and expires on <%= I18n.l @user.reset_password_expires, locale: :en %>. This link works only once and expires on <%= I18n.l @user.reset_password_expires, locale: :en %>.
If you don't want to change your password, just ignore this message. Your password hasn't been changed yet. If you don't want to change your password, just ignore this message. Your password hasn't been changed yet.

View File

@ -1,4 +1,4 @@
- content_for :head do - content_for :javascript do
:javascript :javascript
$(function() { $(function() {
$('#nick').focus(); $('#nick').focus();
@ -7,21 +7,23 @@
- title "FoodSoft login" - title "FoodSoft login"
%noscript %noscript
#javascript-warn.error(style="font-size:1.5em") .alert.alert-error
Achtung, Cookies und Javascript müssen aktiviert sein! Achtung, Cookies und Javascript müssen aktiviert sein!
= link_to "NoScript", "http://noscript.net/" = link_to "NoScript", "http://noscript.net/"
bitte abschalten. bitte abschalten.
#login-form.edit_form(style="width:25em") = form_tag sessions_path, class: 'form-horizontal' do
= form_tag sessions_path do .control-group
%p %label(for='nick' class='control-label') Benutzerin
%label{:for => 'user'} Benutzerin .controls
%br/
= text_field_tag 'nick' = text_field_tag 'nick'
%p
%label{:for => 'password'} Passwort .control-group
%br/ %label(for='password' class='control-label') Passwort
.controls
= password_field_tag 'password' = password_field_tag 'password'
= submit_tag "Anmelden"
| .control-group
= link_to "Passwort vergessen?", :controller => 'login', :action => 'forgot_password' .controls
= submit_tag "Anmelden", class: 'btn'
= link_to "Passwort vergessen?", :controller => 'login', :action => 'forgot_password'

View File

@ -3,11 +3,12 @@
= f.input :last_name = f.input :last_name
= f.input :email = f.input :email
= f.input :phone = f.input :phone
= f.input :password, :required => @user.new_record? = f.input :password, :required => f.object.new_record?
= f.input :password_confirmation = f.input :password_confirmation
- for setting in User::setting_keys.keys .control-group
.input.boolean .controls
= check_box_tag "user[setting_attributes][#{setting}]", - for setting in User::setting_keys.keys
'1', @user.settings[setting] == '1' || @user.settings_default(setting), %label.checkbox{:for => "user[setting_attributes][#{setting}]"}
:class => 'boolean' = check_box_tag "user[setting_attributes][#{setting}]", '1',
%label.boolean{:for => "user[setting_attributes][#{setting}]"}= h User::setting_keys[setting] f.object.settings[setting] == '1' || f.object.settings_default(setting)
= User::setting_keys[setting]

View File

@ -1,2 +1,3 @@
Delayed::Worker.max_attempts = 5 Delayed::Worker.max_attempts = 5
Delayed::Worker.delay_jobs = !Rails.env.test? Delayed::Worker.delay_jobs = !Rails.env.test?
Delayed::Worker.destroy_failed_jobs = false

View File

@ -98,7 +98,7 @@ SimpleForm.setup do |config|
config.label_class = 'control-label' config.label_class = 'control-label'
# You can define the class to use on all forms. Default is simple_form. # You can define the class to use on all forms. Default is simple_form.
# config.form_class = :simple_form config.form_class = 'form-horizontal'
# You can define which elements should obtain additional classes # You can define which elements should obtain additional classes
# config.generate_additional_classes_for = [:wrapper, :label, :input] # config.generate_additional_classes_for = [:wrapper, :label, :input]
@ -108,7 +108,7 @@ SimpleForm.setup do |config|
# Tell browsers whether to use default HTML5 validations (novalidate option). # Tell browsers whether to use default HTML5 validations (novalidate option).
# Default is enabled. # Default is enabled.
config.browser_validations = false config.browser_validations = true
# Collection of methods to detect if a file type was given. # Collection of methods to detect if a file type was given.
# config.file_methods = [ :mounted_as, :file?, :public_filename ] # config.file_methods = [ :mounted_as, :file?, :public_filename ]

View File

@ -0,0 +1,45 @@
# Use this setup block to configure all options available in SimpleForm.
SimpleForm.setup do |config|
config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
b.wrapper :tag => 'div', :class => 'controls' do |ba|
ba.use :input
ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' }
end
end
config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
b.wrapper :tag => 'div', :class => 'controls' do |input|
input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend|
prepend.use :input
end
input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' }
input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
end
end
config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
b.use :html5
b.use :placeholder
b.use :label
b.wrapper :tag => 'div', :class => 'controls' do |input|
input.wrapper :tag => 'div', :class => 'input-append' do |append|
append.use :input
end
input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' }
input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
end
end
# Wrappers for forms and inputs using the Twitter Bootstrap toolkit.
# Check the Bootstrap docs (http://twitter.github.com/bootstrap)
# to learn about the different styles for forms and inputs,
# buttons and other elements.
config.default_wrapper = :bootstrap
end

View File

@ -17,6 +17,8 @@ Foodsoft::Application.routes.draw do
match '/login' => 'sessions#new', :as => 'login' match '/login' => 'sessions#new', :as => 'login'
match '/logout' => 'sessions#destroy', :as => 'logout' match '/logout' => 'sessions#destroy', :as => 'logout'
get '/login/new_password' => 'login#new_password', as: :new_password
match '/login/accept_invitation/:token' => 'login#accept_invitation', as: :accept_invitation
resources :sessions, :only => [:new, :create, :destroy] resources :sessions, :only => [:new, :create, :destroy]
########### User specific ########### User specific
@ -174,6 +176,7 @@ Foodsoft::Application.routes.draw do
resources :users, :only => [:index] resources :users, :only => [:index]
# TODO: This is very error prone. Better deactivate this catch all route
match ':controller(/:action(/:id))(.:format)' match ':controller(/:action(/:id))(.:format)'
end # End of /:foodcoop scope end # End of /:foodcoop scope