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 }