From c5c5b499029609ed691dc8c31d55776316db199d Mon Sep 17 00:00:00 2001 From: Mohamad El-Husseini Date: Mon, 2 Jul 2012 14:46:04 -0400 Subject: [PATCH] Version 2.2.0 --- README.md | 18 ++++++++- controllers/Confirmations.cfc | 54 +++++++++++++++++++++++++++ controllers/Controller.cfc | 12 +++++- controllers/PasswordResets.cfc | 4 +- controllers/Users.cfc | 13 +------ models/User.cfc | 17 ++++++++- views/admin/adminusers/show.cfm | 2 +- views/confirmations/new.cfm | 37 ++++++++++++++++++ views/templates/emailconfirmation.cfm | 9 +++++ views/users/index.cfm | 3 +- views/users/show.cfm | 6 ++- 11 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 controllers/Confirmations.cfc create mode 100644 views/confirmations/new.cfm create mode 100644 views/templates/emailconfirmation.cfm diff --git a/README.md b/README.md index 724b36a..0aad407 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ColdFusion on Wheels User Manager Demo User Manager is a demo app for ColdFusion on Wheels. It's meant to be a toolkit for learning or kickstarting a project that requires session management and authorization functionality. -Current Version 2.1.2 +Current Version 2.2.0 --------------------- Current version includes the following functionality: @@ -14,6 +14,7 @@ Current version includes the following functionality: * CRUD functionality for User model; * Password hashing and salting using bCrypt; * Expiring password resets with confirmation e-mail; +* Email confirmation; * Admin authorization; * Admin CRUD for managing users. * Friendly redirects @@ -21,7 +22,20 @@ Current version includes the following functionality: Change Log ---------- -The following changes have been made in version 2.0: +This change requires a new SQL file (included). The following changes have been made: + +**Version 2.2.0** +* Added a new RESTful Confirmations.cfc controller for confirming email addresses. +* Added two columns in the schema: boolean confirmed, and varchar confirmation token. +* Added new SQL file. +* Refactored how tokens are generated now for password resets and confirmations. Using a stripped UUID as generate secret key was causing bad URLs. +* Added an Admin link if the user is signed in as an admin. +* Moved isAthorized method to Controller.cfc so it can be reused by Confirmations.cfc. +* Added new callback to create a confirmation token when the user signs up. +* Removed dead code and email templates left over from version 1. +* Users#index.cfm now shows confirmation status for users. +* Switched all places from using DateFormat() to a custom formatDate(). This makes changing the date format easier as it's in a single place. + **Version 2.1.2** * Switched password hashing from using a SHA-512 over 1024 iterations to using BCrypt. diff --git a/controllers/Confirmations.cfc b/controllers/Confirmations.cfc new file mode 100644 index 0000000..5ba6efc --- /dev/null +++ b/controllers/Confirmations.cfc @@ -0,0 +1,54 @@ +component + extends="Controller" + hint="Handles email confirmation requests." +{ + /** + * @hint Constructor. + */ + public void function init() { + super.init(); + filters(through="isAuthenticated,isAuthorized,redirectIfConfirmed", except="update"); + } + + // -------------------------------------------------- + // Filters + + /** + * @hint Redirect the user if their email is already confirmed. + */ + public void function redirectIfConfirmed() { + if ( user.confirmed ) { + redirectTo(route="profile", key=user.id, message="Your email address was previously confirmed.", messageType="warning"); + } + } + + // -------------------------------------------------- + // RESTful Actions + + /** + * @hint Renders a page where users request an email with instructions to confirm their email address. + */ + public void function new() {} + + /** + * @hint Sends an email with instructions to confirm email address. + */ + public void function create() { + user.update(emailConfirmationToken = user.generateToken()); + //sendMail(to=user.email, subject="Email confirmation", template="/templates/emailConfirmation", user=user); + redirectTo(route="profile", key=user.id, message="Email sent! Please see email for instructions.", messageType="success"); + } + + /** + * @hint Updates the user to confirmed status. + */ + public void function update() { + user = model("user").findOneByEmailConfirmationToken(params.key); + if ( isObject(user) && user.update(confirmed = 1, emailConfirmationToken = "") ) { + redirectTo(controller="users", action="show", key=user.id, message="Account confirmed successfully.", messageType="success"); + } + else { + redirectTo(route="home", message="No such user.", messageType="error"); + } + } +} \ No newline at end of file diff --git a/controllers/Controller.cfc b/controllers/Controller.cfc index 544c656..e6ad956 100644 --- a/controllers/Controller.cfc +++ b/controllers/Controller.cfc @@ -29,7 +29,17 @@ component redirectTo(route="signIn"); } } - + + /* + * @hint Ensures it's the correct user. + */ + private void function isAuthorized() { + user = model("user").findByKey(params.key); + if ( ! IsObject(user) || ! user.id == currentUser.id ) { + redirectTo(route="home"); + } + } + /** * @hint Redirects the user away if its logged in. */ diff --git a/controllers/PasswordResets.cfc b/controllers/PasswordResets.cfc index 58ff0aa..1389868 100644 --- a/controllers/PasswordResets.cfc +++ b/controllers/PasswordResets.cfc @@ -10,7 +10,7 @@ } // -------------------------------------------------- - // REST + // RESTful Actions /** * @hint Renders the reset form page. @@ -46,7 +46,7 @@ } /** - * @hint Renders the edit user page where users enter a new passwords. + * @hint Updates the user's password. */ public void function update() { user = model("user").findOneByPasswordResetToken(params.key); diff --git a/controllers/Users.cfc b/controllers/Users.cfc index dae0649..42c3129 100644 --- a/controllers/Users.cfc +++ b/controllers/Users.cfc @@ -13,22 +13,12 @@ // -------------------------------------------------- // Filters - /* - * @hint Ensures it's the correct user. - */ - private void function isAuthorized() { - user = model("user").findByKey(params.key); - if ( ! IsObject(user) || ! user.id == currentUser.id ) { - redirectTo(route="home"); - } - } - /* * @hint Ensures the admin setting is set to 0 in case a user tries to exploit mass assignment. */ private void function protectFromMassAssignment() { if ( StructKeyExists(params, "user") ) { - params.user.admin = 0; + params.user.admin = 0; } } @@ -63,6 +53,7 @@ user = model("user").new(params.user); if ( user.save() ) { signIn(user); + //sendMail(to=user.email, subject="Email confirmation", template="/templates/emailConfirmation", user=user); redirectTo(route="profile", key=user.id, message="Account created successfully.", messageType="success"); } else { diff --git a/models/User.cfc b/models/User.cfc index 40ad769..ad89888 100644 --- a/models/User.cfc +++ b/models/User.cfc @@ -6,6 +6,7 @@ * @hint Constructor */ public void function init() { + beforeCreate("setEmailConfirmationToken"); beforeSave("sanitize,securePassword"); validatesConfirmationOf("email,password"); @@ -44,6 +45,13 @@ } } + /** + * @hint Sets the emailConfirmationToken for the user. + */ + private void function setEmailConfirmationToken() { + this.emailConfirmationToken = generateToken(); + } + // -------------------------------------------------- // Public @@ -59,7 +67,7 @@ * @hint Creates a password reset token */ public void function createPasswordResetToken() { - this.passwordResetToken = URLEncodedFormat(GenerateSecretKey("AES", 256)); + this.passwordResetToken = generateToken(); this.passwordResetAt = Now(); this.save(); } @@ -71,4 +79,11 @@ if ( StructKeyExists(this, "password") ) this.password = ""; if ( StructKeyExists(this, "passwordConfirmation") ) this.passwordConfirmation = ""; } + + /** + * @hint Generates a random token. + */ + public string function generateToken() { + return Replace(LCase(CreateUUID()), "-", "", "all"); + } } \ No newline at end of file diff --git a/views/admin/adminusers/show.cfm b/views/admin/adminusers/show.cfm index b2203ce..a93b3f8 100644 --- a/views/admin/adminusers/show.cfm +++ b/views/admin/adminusers/show.cfm @@ -9,7 +9,7 @@
#flashMessageTag()#

#user.name#

-

Joined on #DateFormat(user.createdAt, "medium")#.

+

Joined on #formatDate(user.createdAt)#.

#linkTo(text="← Back", action="index")#

diff --git a/views/confirmations/new.cfm b/views/confirmations/new.cfm new file mode 100644 index 0000000..4b42b2f --- /dev/null +++ b/views/confirmations/new.cfm @@ -0,0 +1,37 @@ + + + #contentFor(pageTitle="Email Confirmation")# + +
+ + +
+
+ #flashMessageTag()# + + +

Your email address has been confirmed previously.

+ +

Click below to receive a new email with instructions for confirming your account.

+
+ + #startFormTag(action="create", key=user.id)# +
+
+ +
+ #textField(objectName="user", property="email", label=false, disabled=true)# +
+
+
+ #submitTag(value="Send Email", class="btn primary")# #linkTo(text="Cancel", controller="sessions", action="new", class="btn")# +
+
+ #endFormTag()# +
+
+
+ +
\ No newline at end of file diff --git a/views/templates/emailconfirmation.cfm b/views/templates/emailconfirmation.cfm new file mode 100644 index 0000000..4c18e5d --- /dev/null +++ b/views/templates/emailconfirmation.cfm @@ -0,0 +1,9 @@ + + +

Dear #user.name#,

+

You need to confirm your e-mail address. Please click on the link below to do so:

+

#URLFor(controller="confirmations", action="update", onlyPath=false, key=user.emailConfirmationToken)#

+

If clicking the above link does not work, just copy it and paste the URL in a new browser window instead.

+

Thanks!

+ +
\ No newline at end of file diff --git a/views/users/index.cfm b/views/users/index.cfm index 946a08e..1c27da7 100644 --- a/views/users/index.cfm +++ b/views/users/index.cfm @@ -18,7 +18,8 @@
#users.name#
-

Joined on #DateFormat(users.createdAt, "medium")#

+

Joined on #formatDate(users.createdAt)#

+

Confirmed: #users.confirmed ? "Yes" : "No"#

#linkTo(text="View Profile", route="profile", key=users.id)#

diff --git a/views/users/show.cfm b/views/users/show.cfm index 603d897..0ae4e93 100644 --- a/views/users/show.cfm +++ b/views/users/show.cfm @@ -18,7 +18,11 @@
#flashMessageTag()# -

Joined on #DateFormat(user.createdAt, "medium")#.

+

Joined on #formatDate(user.createdAt)#.

+ +

Your account has not been confirmed yet. We sent you an email requesting confirmation.

+

#linkTo(text="Click here", controller="confirmations", action="new", key=user.id)# to receive a new email if you can not find it.

+

#linkTo(text="← Back", action="index")#