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("*", &registration.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 }