1 module vibeauth.router.management.responses; 2 3 import std..string; 4 import std.regex; 5 import std.conv; 6 import std.algorithm; 7 8 import vibe.data.json; 9 import vibe.http.router; 10 11 import vibeauth.users; 12 import vibeauth.configuration; 13 import vibeauth.mvc.templatedata; 14 import vibeauth.mvc.view; 15 import vibeauth.mvc.controller; 16 import vibeauth.router.request; 17 import vibeauth.router.management.views; 18 19 20 bool validateRights(HTTPServerRequest req, HTTPServerResponse res, ServiceConfiguration configuration, UserCollection userCollection) { 21 auto logedUser = req.getUser(userCollection); 22 auto path = req.fullURL; 23 24 if(logedUser is null) { 25 res.redirect(path.schema ~ "://" ~ path.host ~ ":" ~ path.port.to!string ~ configuration.paths.login.form, 302); 26 return false; 27 } 28 29 if("userId" !in req.context && !logedUser.can!("admin")) { 30 return false; 31 } 32 33 if("userId" in req.context && logedUser.id != req.context["userId"].to!string && !logedUser.can!("admin")) { 34 return false; 35 } 36 37 return true; 38 } 39 40 class UserController(string configurationPath, View) : PathController!("GET", configurationPath) { 41 protected { 42 User logedUser; 43 } 44 45 /// 46 this(UserCollection userCollection, ServiceConfiguration configuration) { 47 super(userCollection, configuration); 48 } 49 50 private string breadcrumbs() { 51 return `<nav aria-label="breadcrumb"> 52 <ol class="breadcrumb"> 53 <li class="breadcrumb-item"> 54 <a href="#{paths.userManagement.list}">User List</a> 55 </li> 56 <li class="breadcrumb-item active" aria-current="page"> 57 #{userData.name} 58 </li> 59 </ol> 60 </nav>`; 61 } 62 63 void handle(ref View view, User user) { 64 } 65 66 void handle(HTTPServerRequest req, HTTPServerResponse res) { 67 if(!validateRights(req, res, configuration, userCollection)) { 68 return; 69 } 70 71 logedUser = req.getUser(userCollection); 72 73 scope(exit) { 74 logedUser = null; 75 } 76 77 scope auto view = new View(configuration); 78 view.data.set(":id", path, req.path); 79 80 if("message" in req.query) { 81 view.data.addMessage(req.query["message"]); 82 } 83 84 if("error" in req.query) { 85 view.data.addError(req.query["error"]); 86 } 87 88 auto user = userCollection.byId(view.data.get(":id")); 89 view.data.add("userData", user.toJson); 90 91 if(user.getScopes.canFind("admin")) { 92 view.data.add("breadcrumbs", this.breadcrumbs); 93 } else { 94 view.data.add("breadcrumbs", ""); 95 } 96 97 handle(view, user); 98 99 res.writeBody(view.render, 200, "text/html; charset=UTF-8"); 100 } 101 } 102 103 abstract class QuestionController(string configurationPath) : IController { 104 protected { 105 UserCollection userCollection; 106 ServiceConfiguration configuration; 107 string path; 108 } 109 110 this(UserCollection userCollection, ServiceConfiguration configuration) { 111 this.userCollection = userCollection; 112 this.configuration = configuration; 113 114 mixin("path = configuration." ~ configurationPath ~ ";"); 115 } 116 117 bool canHandle(HTTPServerRequest req) { 118 if(req.method != HTTPMethod.GET && req.method != HTTPMethod.POST) { 119 return false; 120 } 121 122 if(!isUserPage(path, req.path)) { 123 return false; 124 } 125 126 TemplateData data; 127 data.set(":id", path, req.path); 128 129 try { 130 userCollection.byId(data.get(":id")); 131 } catch(UserNotFoundException) { 132 return false; 133 } 134 135 req.context["userId"] = data.get(":id"); 136 137 return true; 138 } 139 140 abstract { 141 string title(); 142 string question(); 143 string action(); 144 string backPath(); 145 string backPath(HTTPServerRequest); 146 } 147 148 void handleQuestion(HTTPServerRequest req, HTTPServerResponse res) { 149 auto view = new QuestionView(configuration ); 150 view.data.set(":id", path, req.path); 151 152 view.data.add("title", title()); 153 view.data.add("question", question()); 154 view.data.add("action", action()); 155 view.data.add("path", req.fullURL.toString); 156 view.data.add("path-back", backPath(req)); 157 158 res.writeBody(view.render, 200, "text/html; charset=UTF-8"); 159 } 160 161 abstract void handleAction(HTTPServerRequest req, HTTPServerResponse res); 162 163 void handle(HTTPServerRequest req, HTTPServerResponse res) { 164 if(!validateRights(req, res, configuration, userCollection)) { 165 return; 166 } 167 168 if(req.method == HTTPMethod.GET) { 169 handleQuestion(req, res); 170 } 171 172 if(req.method == HTTPMethod.POST && isValidPassword(req, res)) { 173 handleAction(req, res); 174 } 175 } 176 177 bool isValidPassword(HTTPServerRequest req, HTTPServerResponse res) { 178 auto logedUser = req.getUser(userCollection); 179 180 if(logedUser is null) { 181 res.redirect(backPath(req), 302); 182 return false; 183 } 184 185 auto view = new RedirectView(req, res, backPath); 186 187 if("password" !in req.form) { 188 view.respondError("Can not " ~ title.toLower ~ ". The password was missing."); 189 return false; 190 } 191 192 auto password = req.form["password"]; 193 194 if(!logedUser.isValidPassword(password)) { 195 view.respondError("Can not " ~ title.toLower ~ ". The password was invalid."); 196 return false; 197 } 198 199 return true; 200 } 201 } 202 203 alias ProfileController = UserController!("paths.userManagement.profile", ProfileView); 204 alias AccountController = UserController!("paths.userManagement.account", AccountView); 205 206 class SecurityController : UserController!("paths.userManagement.security", SecurityView) { 207 this(UserCollection userCollection, ServiceConfiguration configuration) { 208 super(userCollection, configuration); 209 } 210 211 override void handle(ref SecurityView view, User user) { 212 auto isAdmin = user.getScopes.canFind("admin"); 213 auto isLogedUser = logedUser.id == user.id; 214 auto isLogedAdmin = logedUser.getScopes.canFind("admin"); 215 216 if(!isLogedAdmin) { 217 view.data.add("rights", ""); 218 return; 219 } 220 221 scope View rightsView; 222 223 if(isLogedUser) { 224 rightsView = new View(configuration.templates.userManagement.adminRights, configuration.serializeToJson); 225 } else { 226 rightsView = new View(configuration.templates.userManagement.otherRights, configuration.serializeToJson); 227 } 228 229 Json roleData = Json.emptyObject; 230 roleData["type"] = isAdmin ? "an administrator" : "not an administrator"; 231 roleData["class"] = isAdmin ? "info" : "secondary"; 232 roleData["action"] = isAdmin ? "revoke admin" : "make admin"; 233 234 auto link = isAdmin ? 235 configuration.paths.userManagement.securityRevokeAdmin : 236 configuration.paths.userManagement.securityMakeAdmin; 237 238 roleData["link"] = link.replace(":id", view.data.get(":id")); 239 240 rightsView.data.add("role", roleData); 241 242 view.data.add("rights", rightsView.render); 243 } 244 } 245 246 class ListController : PathController!("GET", "paths.userManagement.list") { 247 this(UserCollection userCollection, ServiceConfiguration configuration) { 248 super(userCollection, configuration); 249 } 250 251 void handle(HTTPServerRequest req, HTTPServerResponse res) { 252 if(!validateRights(req, res, configuration, userCollection)) { 253 return; 254 } 255 256 scope auto view = new UserManagementListView(configuration, userCollection); 257 res.writeBody(view.render, 200, "text/html; charset=UTF-8"); 258 } 259 } 260 261 class UpdateProfileController : PathController!("POST", "paths.userManagement.updateProfile") { 262 this(UserCollection userCollection, ServiceConfiguration configuration) { 263 super(userCollection, configuration); 264 } 265 266 void handle(HTTPServerRequest req, HTTPServerResponse res) { 267 if(!validateRights(req, res, configuration, userCollection)) { 268 return; 269 } 270 271 auto view = new RedirectView(req, res, configuration.paths.userManagement.profile); 272 273 string id = req.context["userId"].to!string; 274 auto user = userCollection.byId(id); 275 276 if("name" !in req.form || "username" !in req.form) { 277 view.respondError("Missing data. The request can not be processed."); 278 return; 279 } 280 281 string name = req.form["name"].strip.escapeHtmlString; 282 string username = req.form["username"].strip.escapeHtmlString; 283 284 if(username == "") { 285 view.respondError("The username is mandatory."); 286 return; 287 } 288 289 if(username != user.username && userCollection.contains(username)) { 290 view.respondError("The new username is already taken."); 291 return; 292 } 293 294 auto ctr = ctRegex!(`[a-zA-Z][a-zA-Z0-9_\-]*`); 295 auto result = matchFirst(username, ctr); 296 297 if(result.empty || result.front != username) { 298 view.respondError("Username may only contain alphanumeric characters or single hyphens, and it must start with an alphanumeric character."); 299 return; 300 } 301 302 user.name = name; 303 user.username = username; 304 305 view.respondMessage("Profile updated successfully."); 306 } 307 } 308 309 class UpdateAccountController : PathController!("POST", "paths.userManagement.updateAccount") { 310 this(UserCollection userCollection, ServiceConfiguration configuration) { 311 super(userCollection, configuration); 312 } 313 314 void handle(HTTPServerRequest req, HTTPServerResponse res) { 315 if(!validateRights(req, res, configuration, userCollection)) { 316 return; 317 } 318 319 auto view = new RedirectView(req, res, configuration.paths.userManagement.account); 320 321 string id = req.context["userId"].to!string; 322 auto user = userCollection.byId(id); 323 324 string[] missingFields; 325 326 if("oldPassword" !in req.form) { 327 missingFields ~= "oldPassword"; 328 } 329 330 if("newPassword" !in req.form) { 331 missingFields ~= "newPassword"; 332 } 333 334 if("confirmPassword" !in req.form) { 335 missingFields ~= "confirmPassword"; 336 } 337 338 if(missingFields.length > 0) { 339 view.respondError(missingFields.join(" ") ~ " fields are missing."); 340 return; 341 } 342 343 string oldPassword = req.form["oldPassword"]; 344 string newPassword = req.form["newPassword"]; 345 string confirmPassword = req.form["confirmPassword"]; 346 347 if(confirmPassword != newPassword) { 348 view.respondError("Password confirmation doesn't match the password."); 349 return; 350 } 351 352 if(newPassword.length < 10) { 353 view.respondError("The new password is less then 10 chars."); 354 return; 355 } 356 357 if(user.isValidPassword(oldPassword)) { 358 user.setPassword(newPassword); 359 view.respondMessage("Password updated successfully."); 360 } else { 361 view.respondError("The old password is not valid."); 362 } 363 } 364 } 365 366 class DeleteAccountController : QuestionController!("paths.userManagement.deleteAccount") { 367 368 this(UserCollection userCollection, ServiceConfiguration configuration) { 369 super(userCollection, configuration); 370 } 371 372 override { 373 string title() { 374 return "Delete account"; 375 } 376 377 string question() { 378 return "Are you sure you want to delete this account?"; 379 } 380 381 string action() { 382 return "Delete"; 383 } 384 385 string backPath() { 386 return configuration.paths.userManagement.account; 387 } 388 389 string backPath(HTTPServerRequest req) { 390 auto path = req.fullURL; 391 auto destinationPath = backPath.replace(":id", req.context["userId"].to!string); 392 return path.schema ~ "://" ~ path.host ~ ":" ~ path.port.to!string ~ destinationPath; 393 } 394 } 395 396 override void handleAction(HTTPServerRequest req, HTTPServerResponse res) { 397 userCollection.remove(req.context["userId"].to!string); 398 res.redirect(configuration.paths.location, 302); 399 } 400 } 401 402 class RevokeAdminController : QuestionController!("paths.userManagement.securityRevokeAdmin") { 403 404 this(UserCollection userCollection, ServiceConfiguration configuration) { 405 super(userCollection, configuration); 406 } 407 408 override { 409 string title() { 410 return "Revoke admin"; 411 } 412 413 string question() { 414 return "Are you sure you want to revoke the admin rights of this user?"; 415 } 416 417 string action() { 418 return "Revoke"; 419 } 420 421 string backPath() { 422 return configuration.paths.userManagement.security; 423 } 424 425 string backPath(HTTPServerRequest req) { 426 auto path = req.fullURL; 427 auto destinationPath = backPath.replace(":id", req.context["userId"].to!string); 428 return path.schema ~ "://" ~ path.host ~ ":" ~ path.port.to!string ~ destinationPath; 429 } 430 } 431 432 override void handleAction(HTTPServerRequest req, HTTPServerResponse res) { 433 auto view = new RedirectView(req, res, configuration.paths.userManagement.account); 434 435 TemplateData data; 436 data.set(":id", path, req.path); 437 userCollection.byId(data.get(":id")).removeScope("admin"); 438 439 res.redirect(backPath(req), 302); 440 } 441 } 442 443 class MakeAdminController : QuestionController!("paths.userManagement.securityMakeAdmin") { 444 445 this(UserCollection userCollection, ServiceConfiguration configuration) { 446 super(userCollection, configuration); 447 } 448 449 override { 450 string title() { 451 return "Make admin"; 452 } 453 454 string question() { 455 return "Are you sure you want to add admin rights to this user?"; 456 } 457 458 string action() { 459 return "Approve"; 460 } 461 462 string backPath() { 463 return configuration.paths.userManagement.security; 464 } 465 466 string backPath(HTTPServerRequest req) { 467 auto path = req.fullURL; 468 auto destinationPath = backPath.replace(":id", req.context["userId"].to!string); 469 return path.schema ~ "://" ~ path.host ~ ":" ~ path.port.to!string ~ destinationPath; 470 } 471 } 472 473 override void handleAction(HTTPServerRequest req, HTTPServerResponse res) { 474 auto view = new RedirectView(req, res, configuration.paths.userManagement.account); 475 476 TemplateData data; 477 data.set(":id", path, req.path); 478 userCollection.byId(data.get(":id")).addScope("admin"); 479 480 res.redirect(backPath(req), 302); 481 } 482 }