1 /** 2 Web interface implementation 3 4 Copyright: © 2012-2014 RejectedSoftware e.K. 5 License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module userman.web; 9 10 public import userman.controller; 11 12 import vibe.core.log; 13 import vibe.crypto.passwordhash; 14 import vibe.http.router; 15 import vibe.textfilter.urlencode; 16 import vibe.utils.validation; 17 import vibe.web.web; 18 19 import std.exception; 20 21 22 /** 23 Registers the routes for a UserMan web interface. 24 25 Use this to add user management to your web application. See also 26 $(D UserManWebAuthenticator) for some complete examples of a simple 27 web service with UserMan integration. 28 */ 29 void registerUserManWebInterface(URLRouter router, UserManController controller) 30 { 31 router.registerWebInterface(new UserManWebInterface(controller)); 32 } 33 34 35 /** 36 Helper function to update the user profile from a POST request. 37 38 This assumes that the fields are named like they are in userman.profile.dt. 39 Session variables will be updated automatically. 40 */ 41 void updateProfile(UserManController controller, User user, HTTPServerRequest req) 42 { 43 if (controller.settings.useUserNames) { 44 if (auto pv = "name" in req.form) user.fullName = *pv; 45 if (auto pv = "email" in req.form) user.email = *pv; 46 } else { 47 if (auto pv = "email" in req.form) user.email = user.name = *pv; 48 } 49 if (auto pv = "full_name" in req.form) user.fullName = *pv; 50 51 if (auto pv = "password" in req.form) { 52 enforce(user.auth.method == "password", "User account has no password authentication."); 53 auto pconf = "password_confirmation" in req.form; 54 enforce(pconf !is null, "Missing password confirmation."); 55 validatePassword(*pv, *pconf); 56 user.auth.passwordHash = generateSimplePasswordHash(*pv); 57 } 58 59 controller.updateUser(user); 60 61 req.session["userName"] = user.name; 62 req.session["userFullName"] = user.fullName; 63 req.session["userEmail"] = user.email; 64 } 65 66 67 /** 68 Used to privide request authentication for web applications. 69 */ 70 class UserManWebAuthenticator { 71 private { 72 UserManController m_controller; 73 string m_prefix; 74 } 75 76 this(UserManController controller, string prefix = "/") 77 { 78 m_controller = controller; 79 m_prefix = prefix; 80 } 81 82 HTTPServerRequestDelegate auth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback) 83 { 84 void requestHandler(HTTPServerRequest req, HTTPServerResponse res) 85 { 86 if (auto usr = performAuth(req, res)) 87 callback(req, res, usr); 88 } 89 90 return &requestHandler; 91 } 92 HTTPServerRequestDelegate auth(HTTPServerRequestDelegate callback) 93 { 94 return auth((req, res, user){ callback(req, res); }); 95 } 96 97 User performAuth(HTTPServerRequest req, HTTPServerResponse res) 98 { 99 if (!req.session) { 100 res.redirect(m_prefix~"login?redirect="~urlEncode(req.path)); 101 return null; 102 } else { 103 return m_controller.getUserByName(req.session["userName"]); 104 } 105 } 106 107 HTTPServerRequestDelegate ifAuth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback) 108 { 109 void requestHandler(HTTPServerRequest req, HTTPServerResponse res) 110 { 111 if( !req.session ) return; 112 auto usr = m_controller.getUserByName(req.session["userName"]); 113 callback(req, res, usr); 114 } 115 116 return &requestHandler; 117 } 118 } 119 120 /** This example uses the $(D @before) annotation supported by the 121 $(D vibe.web.web) framework for a concise and statically defined 122 authentication approach. 123 */ 124 unittest { 125 import vibe.http.router; 126 import vibe.http.server; 127 import vibe.web.web; 128 129 class MyWebService { 130 private { 131 UserManWebAuthenticator m_auth; 132 } 133 134 this(UserManController userman) 135 { 136 m_auth = new UserManWebAuthenticator(userman); 137 } 138 139 // this route can be accessed publicly (/) 140 void getIndex() 141 { 142 //render!"welcome.dt" 143 } 144 145 // the @authenticated attribute (defined below) ensures that this route 146 // (/private_page) can only ever be accessed when the user is logged in 147 @authenticated 148 void getPrivatePage(User _user) 149 { 150 // render a private page with some user specific information 151 //render!("private_page.dt", _user); 152 } 153 154 // Define a custom attribute for authenticated routes 155 private enum authenticated = before!performAuth("_user"); 156 mixin PrivateAccessProxy; // needed so that performAuth can be private 157 // our custom authentication routine, could return any other type, too 158 private User performAuth(HTTPServerRequest req, HTTPServerResponse res) 159 { 160 return m_auth.performAuth(req, res); 161 } 162 } 163 164 void registerMyService(URLRouter router, UserManController userman) 165 { 166 router.registerUserManWebInterface(userman); 167 router.registerWebInterface(new MyWebService(userman)); 168 } 169 } 170 171 /** An example using a plain $(D vibe.http.router.URLRouter) based 172 authentication approach. 173 */ 174 unittest { 175 import std.functional; // toDelegate 176 import vibe.http.router; 177 import vibe.http.server; 178 179 void getIndex(HTTPServerRequest req, HTTPServerResponse res) 180 { 181 //render!"welcome.dt" 182 } 183 184 void getPrivatePage(HTTPServerRequest req, HTTPServerResponse res, User user) 185 { 186 // render a private page with some user specific information 187 //render!("private_page.dt", _user); 188 } 189 190 void registerMyService(URLRouter router, UserManController userman) 191 { 192 auto authenticator = new UserManWebAuthenticator(userman); 193 router.registerUserManWebInterface(userman); 194 router.get("/", &getIndex); 195 router.any("/private_page", authenticator.auth(toDelegate(&getPrivatePage))); 196 } 197 } 198 199 200 /** Web interface class for UserMan, suitable for use with $(D vibe.web.web). 201 202 The typical approach is to use $(D registerUserManWebInterface) instead of 203 directly using this class. 204 */ 205 class UserManWebInterface { 206 private { 207 UserManController m_controller; 208 UserManWebAuthenticator m_auth; 209 string m_prefix; 210 SessionVar!(string, "userEmail") m_sessUserEmail; 211 SessionVar!(string, "userName") m_sessUserName; 212 SessionVar!(string, "userFullName") m_sessUserFullName; 213 SessionVar!(string, "userID") m_sessUserID; 214 } 215 216 this(UserManController controller, string prefix = "/") 217 { 218 m_controller = controller; 219 m_auth = new UserManWebAuthenticator(controller); 220 m_prefix = prefix; 221 } 222 223 void getLogin(string redirect = "", string _error = "") 224 { 225 string error = _error; 226 auto settings = m_controller.settings; 227 render!("userman.login.dt", error, redirect, settings); 228 } 229 230 @errorDisplay!getLogin 231 void postLogin(string name, string password, string redirect = "") 232 { 233 User user; 234 try { 235 user = m_controller.getUserByEmailOrName(name); 236 enforce(testSimplePasswordHash(user.auth.passwordHash, password), "Wrong password."); 237 } catch (Exception e) { 238 logDebug("Error logging in: %s", e.toString().sanitize); 239 throw new Exception("Invalid user/email or password."); 240 } 241 242 enforce(user.active, "The account is not yet activated."); 243 244 m_sessUserEmail = user.email; 245 m_sessUserName = user.name; 246 m_sessUserFullName = user.fullName; 247 m_sessUserID = user._id.toString(); 248 .redirect(redirect.length ? redirect : m_prefix); 249 } 250 251 void getLogout(HTTPServerResponse res) 252 { 253 terminateSession(); 254 res.headers["Refresh"] = "3; url="~m_controller.settings.serviceUrl.toString(); 255 render!("userman.logout.dt"); 256 } 257 258 void getRegister(string _error = "") 259 { 260 string error = _error; 261 auto settings = m_controller.settings; 262 render!("userman.register.dt", error, settings); 263 } 264 265 @errorDisplay!getRegister 266 void postRegister(ValidEmail email, Nullable!ValidUsername name, string fullName, ValidPassword password, Confirm!"password" passwordConfirmation) 267 { 268 string username; 269 if (m_controller.settings.useUserNames) { 270 enforce(!name.isNull(), "Missing user name field."); 271 username = name; 272 } else username = email; 273 274 m_controller.registerUser(email, username, fullName, password); 275 276 if (m_controller.settings.requireAccountValidation) { 277 string error; 278 render!("userman.register_activate.dt", error); 279 } else { 280 postLogin(name, password); 281 } 282 } 283 284 void getResendActivation(string _error = "") 285 { 286 string error = _error; 287 render!("userman.resend_activation.dt", error); 288 } 289 290 @errorDisplay!getResendActivation 291 void postResendActivation(ValidEmail email) 292 { 293 try { 294 m_controller.resendActivation(email); 295 render!("userman.resend_activation_done.dt"); 296 } catch (Exception e) { 297 logDebug("Error sending activation mail: %s", e.toString().sanitize); 298 throw new Exception("Failed to send activation mail. Please try again later. ("~e.msg~")."); 299 } 300 } 301 302 void getActivate(ValidEmail email, string code) 303 { 304 m_controller.activateUser(email, code); 305 auto user = m_controller.getUserByEmail(email); 306 m_sessUserEmail = user.email; 307 m_sessUserName = user.name; 308 m_sessUserFullName = user.fullName; 309 m_sessUserID = user._id.toString(); 310 render!("userman.activate.dt"); 311 } 312 313 void getForgotLogin(string _error = "") 314 { 315 auto error = _error; 316 render!("userman.forgot_login.dt", error); 317 } 318 319 @errorDisplay!getForgotLogin 320 void postForgotLogin(ValidEmail email) 321 { 322 try { 323 m_controller.requestPasswordReset(email); 324 } catch(Exception e) { 325 // ignore errors, so that registered e-mails cannot be determined 326 logDiagnostic("Failed to send password reset mail to %s: %s", email, e.msg); 327 } 328 329 render!("userman.forgot_login_sent.dt"); 330 } 331 332 void getResetPassword(string _error = "") 333 { 334 string error = _error; 335 render!("userman.reset_password.dt", error); 336 } 337 338 @errorDisplay!getResetPassword 339 void postResetPassword(ValidEmail email, string code, ValidPassword password, Confirm!"password" password_confirmation, HTTPServerResponse res) 340 { 341 m_controller.resetPassword(email, code, password); 342 res.headers["Refresh"] = "3; url=" ~ m_controller.settings.serviceUrl.toString(); 343 render!("userman.reset_password_done.dt"); 344 } 345 346 @auth 347 void getProfile(HTTPServerRequest req, User _user, string _error = "") 348 { 349 req.form["full_name"] = _user.fullName; 350 req.form["email"] = _user.email; 351 bool useUserNames = m_controller.settings.useUserNames; 352 auto user = _user; 353 string error = _error; 354 render!("userman.profile.dt", user, useUserNames, error); 355 } 356 357 @auth @errorDisplay!getProfile 358 void postProfile(HTTPServerRequest req, User _user) 359 { 360 updateProfile(m_controller, _user, req); 361 redirect(m_prefix); 362 } 363 364 // Attribute for authenticated routes 365 private enum auth = before!performAuth("_user"); 366 mixin PrivateAccessProxy; 367 368 private User performAuth(HTTPServerRequest req, HTTPServerResponse res) 369 { 370 return m_auth.performAuth(req, res); 371 } 372 }