1 module vibeauth.router.registration.routes; 2 3 import std.stdio; 4 import std.datetime; 5 import std.algorithm; 6 import std..string; 7 import std.uri; 8 9 import vibe.http.router; 10 import vibe.data.json; 11 import vibe.inet.url; 12 13 import vibeauth.router.registration.responses; 14 import vibeauth.users; 15 import vibeauth.configuration; 16 import vibeauth.mail.base; 17 import vibeauth.challenges.base; 18 import vibeauth.router.accesscontrol; 19 import vibeauth.router.request; 20 import vibeauth.collection; 21 22 /// Handle the registration routes 23 class RegistrationRoutes { 24 25 private { 26 UserCollection collection; 27 IChallenge challenge; 28 IMailQueue mailQueue; 29 RegistrationResponses responses; 30 31 const { 32 ServiceConfiguration configuration; 33 } 34 } 35 36 /// 37 this(UserCollection collection, IChallenge challenge, IMailQueue mailQueue, 38 const ServiceConfiguration configuration = ServiceConfiguration.init) { 39 40 this.collection = collection; 41 this.challenge = challenge; 42 this.mailQueue = mailQueue; 43 this.configuration = configuration; 44 this.responses = new RegistrationResponses(challenge, configuration); 45 } 46 47 /// Handle the requests 48 void handler(HTTPServerRequest req, HTTPServerResponse res) { 49 try { 50 setAccessControl(res); 51 if(req.method == HTTPMethod.OPTIONS) { 52 return; 53 } 54 55 if(req.method == HTTPMethod.GET && req.path == configuration.paths.registration.register) { 56 responses.registerForm(req, res); 57 } 58 59 if(req.method == HTTPMethod.POST && req.path == configuration.paths.registration.addUser) { 60 addUser(req, res); 61 } 62 63 if(req.method == HTTPMethod.GET && req.path == configuration.paths.registration.activation) { 64 activation(req, res); 65 } 66 67 if(req.method == HTTPMethod.POST && req.path == configuration.paths.registration.activation) { 68 newActivation(req, res); 69 } 70 71 if(req.method == HTTPMethod.GET && req.path == configuration.paths.registration.challange) { 72 challenge.generate(req, res); 73 } 74 75 if(req.method == HTTPMethod.GET && req.path == configuration.paths.registration.confirmation) { 76 responses.confirmationForm(req, res); 77 } 78 79 } catch(Exception e) { 80 version(unittest) {} else debug stderr.writeln(e); 81 82 if(!res.headerWritten) { 83 res.writeJsonBody([ "error": ["message": e.msg] ], 500); 84 } 85 } 86 } 87 88 private { 89 /// Activate an account 90 void activation(HTTPServerRequest req, HTTPServerResponse res) 91 { 92 if("token" !in req.query || "email" !in req.query) { 93 res.statusCode = 400; 94 res.writeJsonBody(["error": ["message": "invalid request"]]); 95 96 return; 97 } 98 99 auto token = req.query["token"]; 100 auto email = req.query["email"]; 101 102 if(!collection.contains(email)) { 103 res.statusCode = 400; 104 res.writeJsonBody(["error": ["message": "invalid request"]]); 105 106 return; 107 } 108 109 auto user = collection[email]; 110 111 if(!user.isValidToken(token)) { 112 res.statusCode = 400; 113 res.writeJsonBody(["error": ["message": "invalid request"]]); 114 115 return; 116 } 117 118 user.isActive = true; 119 user.getTokensByType("activation").each!(a => user.revoke(a.name)); 120 121 res.redirect(configuration.paths.registration.activationRedirect); 122 } 123 124 string queryUserData(const RequestUserData userData, string error = "") 125 { 126 string query = "?error=" ~ encodeComponent(error); 127 128 if(userData.name != "") { 129 query ~= "&name=" ~ encodeComponent(userData.name); 130 } 131 132 if(userData.username != "") { 133 query ~= "&username=" ~ encodeComponent(userData.username); 134 } 135 136 if(userData.email != "") { 137 query ~= "&email=" ~ encodeComponent(userData.email); 138 } 139 return query; 140 } 141 142 string[string] activationVariables() 143 { 144 string[string] variables; 145 146 variables["activation"] = configuration.paths.registration.activation; 147 variables["serviceName"] = configuration.name; 148 variables["location"] = configuration.paths.location; 149 150 return variables; 151 } 152 153 void newActivation(HTTPServerRequest req, HTTPServerResponse res) 154 { 155 auto requestData = const RequestUserData(req); 156 157 try { 158 auto user = collection[requestData.email]; 159 160 if(!user.isActive) { 161 auto tokens = user.getTokensByType("activation"); 162 if(!tokens.empty) { 163 user.revoke(tokens.front.name); 164 } 165 166 auto token = collection.createToken(user.email, Clock.currTime + 3600.seconds, [], "activation"); 167 mailQueue.addActivationMessage(user.email, token, activationVariables); 168 } 169 } catch (ItemNotFoundException e) { 170 version(unittest) {{}} else { debug e.writeln; } 171 } 172 173 responses.success(req, res); 174 } 175 176 void addUser(HTTPServerRequest req, HTTPServerResponse res) 177 { 178 immutable bool isJson = req.contentType.toLower.indexOf("json") > -1; 179 auto requestData = const RequestUserData(req); 180 181 try { 182 requestData.validateUser; 183 184 if(!challenge.validate(req, res, requestData.response)) { 185 throw new Exception("Invalid challenge `response`"); 186 } 187 188 if(collection.contains(requestData.email)) { 189 throw new Exception("Email has already been taken"); 190 } 191 192 if(collection.contains(requestData.username)) { 193 throw new Exception("Username has already been taken"); 194 } 195 } catch (Exception e) { 196 if(isJson) { 197 res.statusCode = 400; 198 res.writeJsonBody(["error": ["message": e.msg ]]); 199 } else { 200 res.redirect(configuration.paths.registration.register ~ queryUserData(requestData, e.msg)); 201 } 202 203 return; 204 } 205 206 UserData data; 207 data.name = requestData.name; 208 data.username = requestData.username; 209 data.email = requestData.email; 210 data.isActive = false; 211 212 collection.createUser(data, requestData.password); 213 auto token = collection.createToken(data.email, Clock.currTime + 3600.seconds, [], "activation"); 214 mailQueue.addActivationMessage(requestData.email, token, activationVariables); 215 216 if(isJson) { 217 res.statusCode = 201; 218 res.writeVoidBody; 219 } else { 220 responses.success(req, res); 221 } 222 } 223 } 224 } 225 226 version(unittest) { 227 import std.array; 228 import fluentasserts.vibe.request; 229 import fluentasserts.vibe.json; 230 import fluent.asserts; 231 import vibeauth.token; 232 233 UserMemmoryCollection collection; 234 User user; 235 RegistrationRoutes registration; 236 TestMailQueue mailQueue; 237 Token activationToken; 238 239 alias MailMessage = vibeauth.mail.base.Message; 240 241 class TestMailQueue : MailQueue 242 { 243 MailMessage[] messages; 244 245 this() { 246 super(EmailConfiguration()); 247 } 248 249 override 250 void addMessage(MailMessage message) { 251 messages ~= message; 252 } 253 } 254 255 class TestChallenge : IChallenge { 256 string generate(HTTPServerRequest, HTTPServerResponse) { 257 return "123"; 258 } 259 260 bool validate(HTTPServerRequest, HTTPServerResponse, string response) { 261 return response == "123"; 262 } 263 264 string getTemplate(string challangeLocation) { 265 return ""; 266 } 267 } 268 269 auto testRouter() { 270 auto router = new URLRouter(); 271 mailQueue = new TestMailQueue; 272 273 collection = new UserMemmoryCollection(["doStuff"]); 274 user = new User("user@gmail.com", "password"); 275 user.name = "John Doe"; 276 user.username = "test"; 277 user.id = 1; 278 279 collection.add(user); 280 activationToken = collection.createToken(user.email, Clock.currTime + 3600.seconds, [], "activation"); 281 282 registration = new RegistrationRoutes(collection, new TestChallenge, mailQueue); 283 284 router.any("*", ®istration.handler); 285 return router; 286 } 287 } 288 289 @("POST valid data should create the user") 290 unittest { 291 auto router = testRouter; 292 293 auto data = `{ 294 "name": "test", 295 "username": "test_user", 296 "email": "test@test.com", 297 "password": "testPassword", 298 "response": "123" 299 }`.parseJsonString; 300 301 router 302 .request 303 .post("/register/user") 304 .send(data) 305 .expectStatusCode(200) 306 .end((Response response) => { 307 collection.contains("test@test.com").should.be.equal(true); 308 309 collection["test@test.com"].name.should.equal("test"); 310 collection["test@test.com"].username.should.equal("test_user"); 311 collection["test@test.com"].email.should.equal("test@test.com"); 312 collection["test@test.com"].isActive.should.equal(false); 313 collection["test@test.com"].isValidPassword("testPassword").should.equal(true); 314 315 auto tokens = collection["test@test.com"].getTokensByType("activation").array; 316 317 tokens.length.should.equal(1); 318 collection["test@test.com"].isValidToken(tokens[0].name).should.equal(true); 319 }); 320 } 321 322 @("POST empty password should not create the user") 323 unittest { 324 auto router = testRouter; 325 auto data = `{ 326 "name": "test", 327 "username": "test_user", 328 "email": "test@test.com", 329 "password": "", 330 "response": "123" 331 }`.parseJsonString; 332 333 router 334 .request 335 .header("Content-Type", "application/json") 336 .post("/register/user") 337 .send(data) 338 .expectStatusCode(400) 339 .end((Response response) => { 340 response.bodyJson.keys.should.contain("error"); 341 response.bodyJson["error"].keys.should.contain("message"); 342 }); 343 } 344 345 @("POST short password should not create the user") 346 unittest { 347 auto router = testRouter; 348 349 auto data = `{ 350 "name": "test", 351 "username": "test_user", 352 "email": "test@test.com", 353 "password": "123456789", 354 "response": "123" 355 }`.parseJsonString; 356 357 router 358 .request 359 .header("Content-Type", "application/json") 360 .post("/register/user") 361 .send(data) 362 .expectStatusCode(400) 363 .end((Response response) => { 364 response.bodyJson.keys.should.contain("error"); 365 response.bodyJson["error"].keys.should.contain("message"); 366 }); 367 } 368 369 @("POST with and existing email should fail") 370 unittest { 371 auto router = testRouter; 372 373 auto data = `{ 374 "name": "test", 375 "username": "test", 376 "email": "test_user@gmail.com", 377 "password": "12345678910", 378 "response": "123" 379 }`.parseJsonString; 380 381 router 382 .request 383 .header("Content-Type", "application/json") 384 .post("/register/user") 385 .send(data) 386 .expectStatusCode(400) 387 .end((Response response) => { 388 response.bodyJson.keys.should.contain("error"); 389 response.bodyJson["error"].keys.should.contain("message"); 390 }); 391 } 392 393 @("POST with and existing username should fail") 394 unittest { 395 auto router = testRouter; 396 397 auto data = `{ 398 "name": "test", 399 "username": "test_user", 400 "email": "user@gmail.com", 401 "password": "12345678910", 402 "response": "123" 403 }`.parseJsonString; 404 405 router 406 .request 407 .header("Content-Type", "application/json") 408 .post("/register/user") 409 .send(data) 410 .expectStatusCode(400) 411 .end((Response response) => { 412 response.bodyJson.keys.should.contain("error"); 413 response.bodyJson["error"].keys.should.contain("message"); 414 }); 415 } 416 417 @("POST valid data should send a validation email") 418 unittest { 419 auto router = testRouter; 420 421 auto data = `{ 422 "name": "test", 423 "username": "test_user", 424 "email": "test@test.com", 425 "password": "testPassword", 426 "response": "123" 427 }`.parseJsonString; 428 429 router 430 .request 431 .post("/register/user") 432 .send(data) 433 .expectStatusCode(200) 434 .end((Response response) => { 435 string activationLink = "http://localhost/register/activation?email=test@test.com&token=" 436 ~ collection["test@test.com"].getTokensByType("activation").front.name; 437 438 mailQueue.messages.length.should.equal(1); 439 mailQueue.messages[0].textMessage.should.contain(activationLink); 440 mailQueue.messages[0].htmlMessage.should.contain(`<a href="` ~ activationLink ~ `">`); 441 }); 442 } 443 444 @("GET with valid token should validate the user") 445 unittest { 446 auto router = testRouter; 447 448 collection["user@gmail.com"].isActive.should.equal(false); 449 450 router 451 .request 452 .get("/register/activation?email=user@gmail.com&token=" ~ activationToken.name) 453 .expectStatusCode(302) 454 .end((Response response) => { 455 collection["user@gmail.com"].isValidToken(activationToken.name).should.equal(false); 456 collection["user@gmail.com"].isActive.should.equal(true); 457 }); 458 } 459 460 @("GET with invalid token should not validate the user") 461 unittest { 462 auto router = testRouter; 463 464 collection["user@gmail.com"].isActive.should.equal(false); 465 466 router 467 .request 468 .get("/register/activation?email=user@gmail.com&token=other") 469 .expectStatusCode(400) 470 .end((Response response) => { 471 collection["user@gmail.com"].isValidToken(activationToken.name).should.equal(true); 472 collection["user@gmail.com"].isActive.should.equal(false); 473 }); 474 } 475 476 @("POST with valid email should send a new token to the inactive user") 477 unittest { 478 auto router = testRouter; 479 480 collection["user@gmail.com"].isActive.should.equal(false); 481 482 router 483 .request 484 .post("/register/activation?email=user@gmail.com") 485 .expectStatusCode(200) 486 .end((Response response) => { 487 string activationLink = "http://localhost/register/activation?email=user@gmail.com&token=" 488 ~ collection["user@gmail.com"].getTokensByType("activation").front.name; 489 490 mailQueue.messages.length.should.equal(1); 491 mailQueue.messages[0].textMessage.should.contain(activationLink); 492 mailQueue.messages[0].htmlMessage.should.contain(`<a href="` ~ activationLink ~ `">`); 493 }); 494 } 495 496 @("POST with valid email should not send a new token to the active user") 497 unittest { 498 auto router = testRouter; 499 500 collection["user@gmail.com"].isActive(true); 501 502 router 503 .request 504 .post("/register/activation?email=user@gmail.com") 505 .expectStatusCode(200) 506 .end((Response response) => { 507 mailQueue.messages.length.should.equal(0); 508 }); 509 } 510 511 @("POST with invalid email should respond with 200 page") 512 unittest { 513 auto router = testRouter; 514 515 router 516 .request 517 .post("/register/activation?email=ola.com") 518 .expectStatusCode(200) 519 .end((Response response) => { 520 mailQueue.messages.length.should.equal(0); 521 }); 522 } 523 524 @("POST with missing data should return an error") 525 unittest { 526 auto router = testRouter; 527 528 auto data = `{ 529 "username": "test_user", 530 "email": "test@test.com", 531 "password": "testPassword", 532 "response": "123" 533 }`.parseJsonString; 534 535 router 536 .request 537 .post("/register/user") 538 .send(data) 539 .header("Content-Type", "application/json") 540 .expectStatusCode(400) 541 .end((Response response) => { 542 response.bodyJson.keys.should.contain("error"); 543 response.bodyJson["error"].keys.should.contain("message"); 544 }); 545 546 data = `{ 547 "name": "test", 548 "email": "test@test.com", 549 "password": "testPassword", 550 "response": "123" 551 }`.parseJsonString; 552 553 router 554 .request 555 .post("/register/user") 556 .send(data) 557 .header("Content-Type", "application/json") 558 .expectStatusCode(400) 559 .end((Response response) => { 560 response.bodyJson.keys.should.contain("error"); 561 response.bodyJson["error"].keys.should.contain("message"); 562 }); 563 564 data = `{ 565 "name": "test", 566 "username": "test_user", 567 "password": "testPassword", 568 "response": "123" 569 }`.parseJsonString; 570 571 router 572 .request 573 .post("/register/user") 574 .send(data) 575 .header("Content-Type", "application/json") 576 .expectStatusCode(400) 577 .end((Response response) => { 578 response.bodyJson.keys.should.contain("error"); 579 response.bodyJson["error"].keys.should.contain("message"); 580 }); 581 582 data = `{ 583 "name": "test", 584 "username": "test_user", 585 "email": "test@test.com", 586 "response": "123" 587 }`.parseJsonString; 588 589 router 590 .request 591 .post("/register/user") 592 .send(data) 593 .header("Content-Type", "application/json") 594 .expectStatusCode(400) 595 .end((Response response) => { 596 response.bodyJson.keys.should.contain("error"); 597 response.bodyJson["error"].keys.should.contain("message"); 598 }); 599 600 data = `{ 601 "name": "test", 602 "username": "test_user", 603 "email": "test@test.com", 604 "password": "testPassword" 605 }`.parseJsonString; 606 607 router 608 .request 609 .post("/register/user") 610 .send(data) 611 .header("Content-Type", "application/json") 612 .expectStatusCode(400) 613 .end((Response response) => { 614 response.bodyJson.keys.should.contain("error"); 615 response.bodyJson["error"].keys.should.contain("message"); 616 }); 617 } 618 619 @("POST with wrong response should return an error") 620 unittest { 621 auto router = testRouter; 622 623 auto data = `{ 624 "name": "test", 625 "username": "test_user", 626 "email": "test@test.com", 627 "password": "testPassword", 628 "response": "abc" 629 }`.parseJsonString; 630 631 router 632 .request 633 .post("/register/user") 634 .send(data) 635 .header("Content-Type", "application/json") 636 .expectStatusCode(400) 637 .end((Response response) => { 638 response.bodyJson.keys.should.contain("error"); 639 response.bodyJson["error"].keys.should.contain("message"); 640 }); 641 }