1 /** 2 Web interface implementation 3 4 Copyright: © 2012-2017 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.api; 11 import userman.db.controller : UserManController; 12 import userman.userman : validateUserName; 13 14 import vibe.core.log; 15 import vibe.http.router; 16 import vibe.textfilter.urlencode; 17 import vibe.utils.validation; 18 import vibe.web.auth; 19 import vibe.web.web; 20 21 import std.array : appender; 22 import std.exception; 23 import std.typecons : Nullable; 24 25 26 /** 27 Registers the routes for a UserMan web interface. 28 29 Use this to add user management to your web application. See also 30 $(D UserManWebAuthenticator) for some complete examples of a simple 31 web service with UserMan integration. 32 */ 33 void registerUserManWebInterface(URLRouter router, UserManAPI api) 34 { 35 router.registerWebInterface(new UserManWebInterface(api)); 36 } 37 /// deprecated 38 void registerUserManWebInterface(URLRouter router, UserManController controller) 39 { 40 router.registerUserManWebInterface(createLocalUserManAPI(controller)); 41 } 42 43 44 /** 45 Helper function to update the user profile from a POST request. 46 47 This assumes that the fields are named like they are in userman.profile.dt. 48 Session variables will be updated automatically. 49 */ 50 void updateProfile(UserManAPI api, User.ID user, HTTPServerRequest req) 51 { 52 /*if (api.settings.useUserNames) { 53 if (auto pv = "name" in req.form) { 54 api.users.setName(user, *pv); 55 req.session.set("userName", *pv); 56 } 57 }*/ // TODO! 58 if (auto pv = "email" in req.form) { 59 api.users[user].setEmail(*pv); 60 req.session.set("userEmail", *pv); 61 } 62 if (auto pv = "full_name" in req.form) { 63 api.users[user].setFullName(*pv); 64 req.session.set("userFullName", *pv); 65 } 66 if (auto pv = "password" in req.form) { 67 auto pconf = "password_confirmation" in req.form; 68 enforce(pconf !is null, "Missing password confirmation."); 69 validatePassword(*pv, *pconf); 70 api.users[user].setPassword(*pv); 71 } 72 } 73 /// ditto 74 deprecated void updateProfile(UserManController controller, User user, HTTPServerRequest req) 75 { 76 updateProfile(createLocalUserManAPI(controller), user.id, req); 77 } 78 79 80 /** 81 Used to provide request authentication for web applications. 82 83 Note that it is generally recommended to use the `vibe.web.auth` mechanism 84 together with `authenticate` instead of using this class. 85 86 See_also: `authenticate` 87 */ 88 class UserManWebAuthenticator { 89 private { 90 UserManAPI m_api; 91 string m_prefix; 92 } 93 94 this(UserManAPI api, string prefix = "/") 95 { 96 m_api = api; 97 m_prefix = prefix; 98 } 99 100 deprecated this(UserManController controller, string prefix = "/") 101 { 102 this(createLocalUserManAPI(controller), prefix); 103 } 104 105 HTTPServerRequestDelegate auth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback) 106 { 107 void requestHandler(HTTPServerRequest req, HTTPServerResponse res) 108 @trusted { 109 User usr; 110 try usr = performAuth(req, res); 111 catch (Exception e) throw new HTTPStatusException(HTTPStatus.unauthorized); 112 if (res.headerWritten) return; 113 callback(req, res, usr); 114 } 115 116 return &requestHandler; 117 } 118 HTTPServerRequestDelegate auth(HTTPServerRequestDelegate callback) 119 { 120 return auth((req, res, user){ callback(req, res); }); 121 } 122 123 User performAuth(HTTPServerRequest req, HTTPServerResponse res) 124 { 125 if (!req.session) { 126 res.redirect(m_prefix~"login?redirect="~urlEncode(req.path)); 127 return User.init; 128 } else { 129 return m_api.users.getByName(req.session.get!string("userName")); 130 } 131 } 132 133 HTTPServerRequestDelegate ifAuth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback) 134 { 135 void requestHandler(HTTPServerRequest req, HTTPServerResponse res) 136 @trusted { 137 if( !req.session ) return; 138 auto usr = m_api.users.getByName(req.session.get!string("userName")); 139 callback(req, res, usr); 140 } 141 142 return &requestHandler; 143 } 144 } 145 146 /** An example using a plain $(D vibe.http.router.URLRouter) based 147 authentication approach. 148 */ 149 unittest { 150 import std.functional; // toDelegate 151 import vibe.http.router; 152 import vibe.http.server; 153 154 void getIndex(HTTPServerRequest req, HTTPServerResponse res) 155 { 156 //render!"welcome.dt" 157 } 158 159 void getPrivatePage(HTTPServerRequest req, HTTPServerResponse res, User user) 160 { 161 // render a private page with some user specific information 162 //render!("private_page.dt", _user); 163 } 164 165 void registerMyService(URLRouter router, UserManAPI userman) 166 { 167 auto authenticator = new UserManWebAuthenticator(userman); 168 router.registerUserManWebInterface(userman); 169 router.get("/", &getIndex); 170 router.any("/private_page", authenticator.auth(toDelegate(&getPrivatePage))); 171 } 172 } 173 174 175 /** Ensures that a user is logged in. 176 177 If a valid session exists, the returned `User` object will contain all 178 information about the logged in user. Otherwise, the response object will 179 be used to redirect the user to the login page and an empty user object 180 is returned. 181 182 Params: 183 req = The request object of the incoming request 184 res = The response object of the incoming request 185 api = A reference to the UserMan API 186 url_prefix = Optional prefix to prepend to the login page path 187 */ 188 User authenticate(HTTPServerRequest req, HTTPServerResponse res, UserManAPI api, string url_prefix = "") 189 @trusted { 190 if (!req.session) { 191 res.redirect(url_prefix~"login?redirect="~urlEncode(req.path)); 192 return User.init; 193 } else { 194 return api.users.getByName(req.session.get!string("userName")); 195 } 196 } 197 198 /** This example uses the $(D @before) annotation supported by the 199 $(D vibe.web.web) framework for a concise and statically defined 200 authentication approach. 201 */ 202 unittest { 203 import vibe.http.router; 204 import vibe.http.server; 205 import vibe.web.web; 206 207 @requiresAuth 208 class MyWebService { 209 @safe: 210 private { 211 UserManAPI m_api; 212 } 213 214 this(UserManAPI userman) 215 { 216 m_api = userman; 217 } 218 219 // this route can be accessed publicly (/) 220 @noAuth 221 void getIndex() 222 { 223 //render!"welcome.dt" 224 } 225 226 // the @authenticated attribute (defined below) ensures that this route 227 // (/private_page) can only ever be accessed if the user is logged in 228 @anyAuth 229 void getPrivatePage(User user) 230 { 231 // render a private page with some user specific information 232 //render!("private_page.dt", user); 233 } 234 235 @noRoute User authenticate(HTTPServerRequest req, HTTPServerResponse res) 236 { 237 return .authenticate(req, res, m_api); 238 } 239 } 240 241 void registerMyService(URLRouter router, UserManAPI userman) 242 { 243 router.registerUserManWebInterface(userman); 244 router.registerWebInterface(new MyWebService(userman)); 245 } 246 } 247 248 249 /** Web interface class for UserMan, suitable for use with $(D vibe.web.web). 250 251 The typical approach is to use $(D registerUserManWebInterface) instead of 252 directly using this class. 253 */ 254 @requiresAuth 255 @translationContext!TranslationContext 256 class UserManWebInterface { 257 private { 258 UserManAPI m_api; 259 string m_prefix; 260 SessionVar!(string, "userEmail") m_sessUserEmail; 261 SessionVar!(string, "userName") m_sessUserName; 262 SessionVar!(string, "userFullName") m_sessUserFullName; 263 SessionVar!(string, "userID") m_sessUserID; 264 UserManAPISettings m_settings; 265 } 266 267 this(UserManAPI api, string prefix = "/") 268 { 269 m_api = api; 270 m_settings = api.settings; 271 m_prefix = prefix; 272 } 273 274 deprecated this(UserManController controller, string prefix = "/") 275 { 276 this(createLocalUserManAPI(controller), prefix); 277 } 278 279 @noAuth 280 void getLogin(string redirect = "", string _error = "") 281 { 282 string error = _error; 283 auto settings = m_settings; 284 render!("userman.login.dt", error, redirect, settings); 285 } 286 287 @noAuth @errorDisplay!getLogin 288 void postLogin(string name, string password, string redirect = "") 289 { 290 User user; 291 try { 292 auto uid = m_api.users.testLogin(name, password); 293 user = m_api.users[uid].get(); 294 } catch (Exception e) { 295 import std.encoding : sanitize; 296 logDebug("Error logging in: %s", e.toString().sanitize); 297 throw new Exception("Invalid user/email or password."); 298 } 299 300 enforce(user.active, "The account is not yet activated."); 301 302 m_sessUserEmail = user.email; 303 m_sessUserName = user.name; 304 m_sessUserFullName = user.fullName; 305 m_sessUserID = user.id.toString(); 306 .redirect(redirect.length ? redirect : m_prefix); 307 } 308 309 @noAuth 310 void getLogout(HTTPServerResponse res) 311 { 312 terminateSession(); 313 res.headers["Refresh"] = "3; url="~m_settings.serviceURL.toString(); 314 render!("userman.logout.dt"); 315 } 316 317 @noAuth 318 void getRegister(string _error = "") 319 { 320 string error = _error; 321 auto settings = m_settings; 322 render!("userman.register.dt", error, settings); 323 } 324 325 @noAuth @errorDisplay!getRegister 326 void postRegister(ValidEmail email, Nullable!string name, string fullName, ValidPassword password, Confirm!"password" passwordConfirmation) 327 { 328 string username; 329 if (m_settings.useUserNames) { 330 enforce(!name.isNull, "Missing user name field."); 331 332 auto err = appender!string(); 333 enforce(m_settings.userNameSettings.validateUserName(err, name), err.data); 334 335 username = name; 336 } else username = email; 337 338 m_api.users.register(email, username, fullName, password); 339 340 if (m_settings.requireActivation) { 341 string error; 342 render!("userman.register_activate.dt", error); 343 } else { 344 postLogin(username, password); 345 } 346 } 347 348 @noAuth 349 void getResendActivation(string _error = "") 350 { 351 string error = _error; 352 render!("userman.resend_activation.dt", error); 353 } 354 355 @noAuth @errorDisplay!getResendActivation 356 void postResendActivation(ValidEmail email) 357 { 358 try { 359 m_api.users.resendActivation(email); 360 render!("userman.resend_activation_done.dt"); 361 } catch (Exception e) { 362 import std.encoding : sanitize; 363 logDebug("Error sending activation mail: %s", e.toString().sanitize); 364 throw new Exception("Failed to send activation mail. Please try again later. ("~e.msg~")."); 365 } 366 } 367 368 @noAuth 369 void getActivate(ValidEmail email, string code) 370 { 371 m_api.users.activate(email, code); 372 auto user = m_api.users.getByEmail(email); 373 m_sessUserEmail = user.email; 374 m_sessUserName = user.name; 375 m_sessUserFullName = user.fullName; 376 m_sessUserID = user.id.toString(); 377 render!("userman.activate.dt"); 378 } 379 380 @noAuth 381 void getForgotLogin(string _error = "") 382 { 383 auto error = _error; 384 render!("userman.forgot_login.dt", error); 385 } 386 387 @noAuth @errorDisplay!getForgotLogin 388 void postForgotLogin(ValidEmail email) 389 { 390 try { 391 m_api.users.requestPasswordReset(email); 392 } catch(Exception e) { 393 // ignore errors, so that registered e-mails cannot be determined 394 logDiagnostic("Failed to send password reset mail to %s: %s", email, e.msg); 395 } 396 397 render!("userman.forgot_login_sent.dt"); 398 } 399 400 @noAuth 401 void getResetPassword(string _error = "") 402 { 403 string error = _error; 404 render!("userman.reset_password.dt", error); 405 } 406 407 @noAuth @errorDisplay!getResetPassword 408 void postResetPassword(ValidEmail email, string code, ValidPassword password, Confirm!"password" password_confirmation, HTTPServerResponse res) 409 { 410 m_api.users.resetPassword(email, code, password); 411 res.headers["Refresh"] = "3; url=" ~ m_settings.serviceURL.toString(); 412 render!("userman.reset_password_done.dt"); 413 } 414 415 @anyAuth 416 void getProfile(HTTPServerRequest req, User _user, string _error = "") 417 { 418 req.form["full_name"] = _user.fullName; 419 req.form["email"] = _user.email; 420 bool useUserNames = m_settings.useUserNames; 421 auto user = _user; 422 string error = _error; 423 render!("userman.profile.dt", user, useUserNames, error); 424 } 425 426 @anyAuth @errorDisplay!getProfile 427 void postProfile(HTTPServerRequest req, User _user) 428 { 429 updateProfile(m_api, _user.id, req); 430 redirect(m_prefix); 431 } 432 433 @noRoute User authenticate(HTTPServerRequest req, HTTPServerResponse res) 434 @safe { 435 return .authenticate(req, res, m_api, m_prefix); 436 } 437 } 438 439 private struct TranslationContext { 440 import std.meta : AliasSeq; 441 alias languages = AliasSeq!("en_US", "de_DE"); 442 //mixin translationModule!"userman"; 443 }