Permalink
Browse files

Password recovery.

  • Loading branch information...
1 parent 2b1fea0 commit e6d5f3b78dd5d2932ad606ced6655624d146bc52 @antirez committed Oct 10, 2012
Showing with 225 additions and 2 deletions.
  1. +124 −0 app.rb
  2. +6 −0 app_config.rb
  3. +73 −0 mail.rb
  4. +2 −2 page.rb
  5. +20 −0 public/js/app.js
View
124 app.rb
@@ -36,6 +36,7 @@
require 'digest/md5'
require 'comments'
require 'pbkdf2'
+require 'mail'
require 'openssl' if UseOpenSSL
Version = "0.10.0"
@@ -195,6 +196,7 @@
}
}+
H.div(:id => "errormsg"){}+
+ H.a(:href=>"/reset-password") {"reset password"}+
H.script() {'
$(function() {
$("form[name=f]").submit(login);
@@ -203,6 +205,74 @@
}
end
+get '/reset-password' do
+ H.set_title "Reset Password - #{SiteName}"
+ H.page {
+ H.p {
+ "Welcome to the password reset procedure. Please specify the username and the email address you used to register to the site. "+H.br+
+ H.b {"Note that if you did not specified an email it is impossible for you to recover your password."}
+ }+
+ H.div(:id => "login") {
+ H.form(:name=>"f") {
+ H.label(:for => "username") {"username"}+
+ H.inputtext(:id => "username", :name => "username")+
+ H.label(:for => "password") {"email"}+
+ H.inputtext(:id => "email", :name => "email")+H.br+
+ H.submit(:name => "do_reset", :value => "Reset password")
+ }
+ }+
+ H.div(:id => "errormsg"){}+
+ H.script() {'
+ $(function() {
+ $("form[name=f]").submit(reset_password);
+ });
+ '}
+ }
+end
+
+get '/reset-password-ok' do
+ H.set_title "Reset link sent to your inbox"
+ H.page {
+ H.p {
+ "We sent an email to your inbox with a link that will let you reset your password."
+ }+
+ H.p {
+ "Please make sure to check the spam folder if the email will not appear in your inbox in a few minutes."
+ }+
+ H.p {
+ "The email contains a link that will automatically log into your account where you can set a new password in the account preferences."
+ }
+ }
+end
+
+get '/set-new-password' do
+ redirect '/' if (!check_params "user","auth")
+ user = get_user_by_username(params[:user])
+ if !user || user['auth'] != params[:auth]
+ H.page {
+ H.p {
+ "Link invalid or expired."
+ }
+ }
+ else
+ # Login the user and bring him to preferences to set a new password.
+ # Note that we update the auth token so this reset link will not
+ # work again.
+ update_auth_token(user["id"])
+ user = get_user_by_id(user["id"])
+ H.page {
+ H.script() {"
+ $(function() {
+ document.cookie =
+ 'auth=#{user['auth']}'+
+ '; expires=Thu, 1 Aug 2030 20:00:00 UTC; path=/';
+ window.location.href = '/user/#{user['username']}';
+ });
+ "}
+ }
+ end
+end
+
get '/submit' do
redirect "/login" if !$user
H.set_title "Submit a new story - #{SiteName}"
@@ -564,6 +634,47 @@ def render_comment_subthread(comment,sep="")
end
end
+get '/api/reset-password' do
+ content_type 'application/json'
+ if (!check_params "username","email")
+ return {
+ :status => "err",
+ :error => "Username and email are two required fields."
+ }.to_json
+ end
+
+ user = get_user_by_username(params[:username])
+ if user && user['email'] && user['email'] == params[:email]
+ id = user['id']
+ # Rate limit password reset attempts.
+ if (user['pwd_reset'] &&
+ (Time.now.to_i - user['pwd_reset'].to_i) < PasswordResetDelay)
+ return {
+ :status => "err",
+ :error => "Sorry, not enough time elapsed since last password reset request."
+ }.to_json
+ end
+
+ if send_reset_password_email(user)
+ # All fine, set the last password reset time to the current time
+ # for rate limiting purposes, and send the email with the reset
+ # link.
+ $r.hset("user:#{id}","pwd_reset",Time.now.to_i)
+ return {:status => "ok"}.to_json
+ else
+ return {
+ :status => "err",
+ :error => "Problem sending the email, please contact the site admin."
+ }.to_json
+ end
+ else
+ return {
+ :status => "err",
+ :error => "No match for the specified username / email pair."
+ }.to_json
+ end
+end
+
post '/api/create_account' do
content_type 'application/json'
if (!check_params "username","password")
@@ -1116,6 +1227,19 @@ def user_is_admin?(user)
user_has_flags?(user,"a")
end
+def send_reset_password_email(user)
+ return false if !MailRelay || !MailFrom
+ aux = request.url.split("/")
+ return false if aux.length < 3
+ current_domain = aux[0]+"//"+aux[2]
+
+ reset_link = "#{current_domain}/set-new-password?user=#{H.urlencode(user['username'])}&auth=#{H.urlencode(user['auth'])}"
+
+ subject = "#{aux[2]} password reset"
+ message = "You can reset your password here: #{reset_link}"
+ return mail(MailRelay,MailFrom,user['email'],subject,message)
+end
+
################################################################################
# News
################################################################################
View
@@ -34,6 +34,7 @@
# User
DeletedUser = {"username" => "deleted_user", "email" => "", "id" => -1}
+PasswordResetDelay = 3600*24
# News and ranking
NewsAgePadding = 3600*8
@@ -54,3 +55,8 @@
# API
APIMaxNewsCount = 32
+
+# Email service. Set MailRelay to false to disable this functionality
+# (this will prevent users from recovery accounts if the password gets lost).
+MailRelay = "localhost"
+MailFrom = "noreply@example.com"
View
73 mail.rb
@@ -0,0 +1,73 @@
+require 'net/smtp'
+
+# Check if an email is valid, in a not very future-proof way.
+def is_valid_email?(mail)
+ # Characters allowed on name: 0-9a-Z-._ on host: 0-9a-Z-. on between: @
+ return false if mail !~ /^[0-9a-zA-Z\.\-\_\+]+\@[0-9a-zA-Z\.\-]+$/
+
+ # Must start or end with alpha or num
+ return false if mail =~ /^[^0-9a-zA-Z]|[^0-9a-zA-Z]$/
+
+ # Name must end with alpha or num
+ return false if mail !~ /([0-9a-zA-Z]{1})\@./
+
+ # Host must start with alpha or num
+ return false if mail !~ /.\@([0-9a-zA-Z]{1})/
+
+ # Host must end with '.' plus 2 or 3 or 4 alpha for TopLevelDomain
+ # (MUST be modified in future!)
+ return false if mail !~ /\.([a-zA-Z]{2,4})$/
+
+ return true
+end
+
+# Send an email using the specified SMTP relay host.
+#
+# 'relay' is an IP address or hostname of an SMTP server.
+# 'from' can be a string or a two elements array [name,address].
+# 'to' is a comma separated list of recipients.
+# 'subject' and 'body' are just strings.
+#
+# If opt[:html] is true a set of headers to send HTML emails are emitted.
+#
+# The function does not try to send emails to destination addresses that
+# appear to be invald. If at least one error occurs sending the email, then
+# false is returned and the operation aborted, otherwise true is returned.
+def mail(relay,from,to,subject,body,opt={})
+ header=''
+ if opt[:html]
+ header << "MIME-Version: 1.0\r\n"
+ header << "Content-type: text/html;"
+ header << "charset=utf-8\r\n"
+ end
+
+ if from.is_a?(Array)
+ header << "From: "+from[0]+" <"+from[1]+">"
+ from=from[1]
+ else
+ header << "From: "+from
+ end
+
+message = <<END_OF_MESSAGE
+#{header}
+To: #{to}
+Subject: #{subject}
+
+#{body}
+END_OF_MESSAGE
+
+ status = true
+ Net::SMTP.start(relay) {|smtp|
+ to.split(",").each {|dest|
+ dest = dest.strip
+ if is_valid_email? dest
+ smtp.send_message(message,from,dest)
+ else
+ status = false
+ end
+ }
+ }
+ return status
+rescue Exception => e
+ return false
+end
View
@@ -128,11 +128,11 @@ def page()
self.meta(:charset => "utf-8")+
self.title{H.entities @title}+
self.meta(:content => :index, :name => :robots)+
- self.link(:href => "/css/style.css?v=9", :rel => "stylesheet",
+ self.link(:href => "/css/style.css?v=10", :rel => "stylesheet",
:type => "text/css")+
self.link(:href => "/images/favicon.png", :rel => "shortcut icon")+
self.script(:src => "/js/jquery.1.6.4.min.js"){}+
- self.script(:src => "/js/app.js?v=8"){}
+ self.script(:src => "/js/app.js?v=10"){}
}+
self.body {
self.div(:class => "container") {
View
@@ -22,6 +22,26 @@ function login() {
return false;
}
+function reset_password() {
+ var data = {
+ username: $("input[name=username]").val(),
+ email: $("input[name=email]").val(),
+ };
+ $.ajax({
+ type: "GET",
+ url: "/api/reset-password",
+ data: data,
+ success: function(r) {
+ if (r.status == "ok") {
+ window.location.href = "/reset-password-ok";
+ } else {
+ $("#errormsg").html(r.error)
+ }
+ }
+ });
+ return false;
+}
+
function submit() {
var data = {
news_id: $("input[name=news_id]").val(),

0 comments on commit e6d5f3b

Please sign in to comment.