1 /**
2 	Web interface implementation
3 
4 	Copyright: © 2012-2014 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.controller;
11 
12 import vibe.core.log;
13 import vibe.crypto.passwordhash;
14 import vibe.http.router;
15 import vibe.textfilter.urlencode;
16 import vibe.utils.validation;
17 import vibe.web.web;
18 
19 import std.exception;
20 
21 
22 /**
23 	Registers the routes for a UserMan web interface.
24 
25 	Use this to add user management to your web application. See also
26 	$(D UserManWebAuthenticator) for some complete examples of a simple
27 	web service with UserMan integration.
28 */
29 void registerUserManWebInterface(URLRouter router, UserManController controller)
30 {
31 	router.registerWebInterface(new UserManWebInterface(controller));
32 }
33 
34 
35 /**
36 	Helper function to update the user profile from a POST request.
37 
38 	This assumes that the fields are named like they are in userman.profile.dt.
39 	Session variables will be updated automatically.
40 */
41 void updateProfile(UserManController controller, User user, HTTPServerRequest req)
42 {
43 	if (controller.settings.useUserNames) {
44 		if (auto pv = "name" in req.form) user.fullName = *pv;
45 		if (auto pv = "email" in req.form) user.email = *pv;
46 	} else {
47 		if (auto pv = "email" in req.form) user.email = user.name = *pv;
48 	}
49 	if (auto pv = "full_name" in req.form) user.fullName = *pv;
50 
51 	if (auto pv = "password" in req.form) {
52 		enforce(user.auth.method == "password", "User account has no password authentication.");
53 		auto pconf = "password_confirmation" in req.form;
54 		enforce(pconf !is null, "Missing password confirmation.");
55 		validatePassword(*pv, *pconf);
56 		user.auth.passwordHash = generateSimplePasswordHash(*pv);
57 	}
58 
59 	controller.updateUser(user);
60 
61 	req.session["userName"] = user.name;
62 	req.session["userFullName"] = user.fullName;
63 	req.session["userEmail"] = user.email;
64 }
65 
66 
67 /**
68 	Used to privide request authentication for web applications.
69 */
70 class UserManWebAuthenticator {
71 	private {
72 		UserManController m_controller;
73 		string m_prefix;
74 	}
75 
76 	this(UserManController controller, string prefix = "/")
77 	{
78 		m_controller = controller;
79 		m_prefix = prefix;
80 	}
81 
82 	HTTPServerRequestDelegate auth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback)
83 	{
84 		void requestHandler(HTTPServerRequest req, HTTPServerResponse res)
85 		{
86 			if (auto usr = performAuth(req, res))
87 				callback(req, res, usr);
88 		}
89 		
90 		return &requestHandler;
91 	}
92 	HTTPServerRequestDelegate auth(HTTPServerRequestDelegate callback)
93 	{
94 		return auth((req, res, user){ callback(req, res); });
95 	}
96 
97 	User performAuth(HTTPServerRequest req, HTTPServerResponse res)
98 	{
99 		if (!req.session) {
100 			res.redirect(m_prefix~"login?redirect="~urlEncode(req.path));
101 			return null;
102 		} else {
103 			return m_controller.getUserByName(req.session["userName"]);
104 		}
105 	}
106 	
107 	HTTPServerRequestDelegate ifAuth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback)
108 	{
109 		void requestHandler(HTTPServerRequest req, HTTPServerResponse res)
110 		{
111 			if( !req.session ) return;
112 			auto usr = m_controller.getUserByName(req.session["userName"]);
113 			callback(req, res, usr);
114 		}
115 		
116 		return &requestHandler;
117 	}
118 }
119 
120 /** This example uses the $(D @before) annotation supported by the
121 	$(D vibe.web.web) framework for a concise and statically defined
122 	authentication approach.
123 */
124 unittest {
125 	import vibe.http.router;
126 	import vibe.http.server;
127 	import vibe.web.web;
128 
129 	class MyWebService {
130 		private {
131 			UserManWebAuthenticator m_auth;
132 		}
133 
134 		this(UserManController userman)
135 		{
136 			m_auth = new UserManWebAuthenticator(userman);
137 		}
138 
139 		// this route can be accessed publicly (/)
140 		void getIndex()
141 		{
142 			//render!"welcome.dt"
143 		}
144 
145 		// the @authenticated attribute (defined below) ensures that this route
146 		// (/private_page) can only ever be accessed when the user is logged in
147 		@authenticated
148 		void getPrivatePage(User _user)
149 		{
150 			// render a private page with some user specific information
151 			//render!("private_page.dt", _user);
152 		}
153 
154 		// Define a custom attribute for authenticated routes
155 		private enum authenticated = before!performAuth("_user");
156 		mixin PrivateAccessProxy; // needed so that performAuth can be private
157 		// our custom authentication routine, could return any other type, too
158 		private User performAuth(HTTPServerRequest req, HTTPServerResponse res)
159 		{
160 			return m_auth.performAuth(req, res);
161 		}
162 	}
163 
164 	void registerMyService(URLRouter router, UserManController userman)
165 	{
166 		router.registerUserManWebInterface(userman);
167 		router.registerWebInterface(new MyWebService(userman));
168 	}
169 }
170 
171 /** An example using a plain $(D vibe.http.router.URLRouter) based
172 	authentication approach.
173 */
174 unittest {
175 	import std.functional; // toDelegate
176 	import vibe.http.router;
177 	import vibe.http.server;
178 
179 	void getIndex(HTTPServerRequest req, HTTPServerResponse res)
180 	{
181 		//render!"welcome.dt"
182 	}
183 
184 	void getPrivatePage(HTTPServerRequest req, HTTPServerResponse res, User user)
185 	{
186 		// render a private page with some user specific information
187 		//render!("private_page.dt", _user);
188 	}
189 
190 	void registerMyService(URLRouter router, UserManController userman)
191 	{
192 		auto authenticator = new UserManWebAuthenticator(userman);
193 		router.registerUserManWebInterface(userman);
194 		router.get("/", &getIndex);
195 		router.any("/private_page", authenticator.auth(toDelegate(&getPrivatePage)));
196 	}
197 }
198 
199 
200 /** Web interface class for UserMan, suitable for use with $(D vibe.web.web).
201 
202 	The typical approach is to use $(D registerUserManWebInterface) instead of
203 	directly using this class.
204 */
205 class UserManWebInterface {
206 	private {
207 		UserManController m_controller;
208 		UserManWebAuthenticator m_auth;
209 		string m_prefix;
210 		SessionVar!(string, "userEmail") m_sessUserEmail;
211 		SessionVar!(string, "userName") m_sessUserName;
212 		SessionVar!(string, "userFullName") m_sessUserFullName;
213 		SessionVar!(string, "userID") m_sessUserID;
214 	}
215 	
216 	this(UserManController controller, string prefix = "/")
217 	{
218 		m_controller = controller;
219 		m_auth = new UserManWebAuthenticator(controller);
220 		m_prefix = prefix;
221 	}
222 	
223 	void getLogin(string redirect = "", string _error = "")
224 	{
225 		string error = _error;
226 		auto settings = m_controller.settings;
227 		render!("userman.login.dt", error, redirect, settings);
228 	}
229 
230 	@errorDisplay!getLogin	
231 	void postLogin(string name, string password, string redirect = "")
232 	{
233 		User user;
234 		try {
235 			user = m_controller.getUserByEmailOrName(name);
236 			enforce(testSimplePasswordHash(user.auth.passwordHash, password), "Wrong password.");
237 		} catch (Exception e) {
238 			logDebug("Error logging in: %s", e.toString().sanitize);
239 			throw new Exception("Invalid user/email or password.");
240 		}
241 
242 		enforce(user.active, "The account is not yet activated.");
243 		
244 		m_sessUserEmail = user.email;
245 		m_sessUserName = user.name;
246 		m_sessUserFullName = user.fullName;
247 		m_sessUserID = user._id.toString();
248 		.redirect(redirect.length ? redirect : m_prefix);
249 	}
250 	
251 	void getLogout(HTTPServerResponse res)
252 	{
253 		terminateSession();
254 		res.headers["Refresh"] = "3; url="~m_controller.settings.serviceUrl.toString();
255 		render!("userman.logout.dt");
256 	}
257 
258 	void getRegister(string _error = "")
259 	{
260 		string error = _error;
261 		auto settings = m_controller.settings;
262 		render!("userman.register.dt", error, settings);
263 	}
264 	
265 	@errorDisplay!getRegister
266 	void postRegister(ValidEmail email, Nullable!ValidUsername name, string fullName, ValidPassword password, Confirm!"password" passwordConfirmation)
267 	{
268 		string username;
269 		if (m_controller.settings.useUserNames) {
270 			enforce(!name.isNull(), "Missing user name field.");
271 			username = name;
272 		} else username = email;
273 
274 		m_controller.registerUser(email, username, fullName, password);
275 
276 		if (m_controller.settings.requireAccountValidation) {
277 			string error;
278 			render!("userman.register_activate.dt", error);
279 		} else {
280 			postLogin(name, password);
281 		}
282 	}
283 	
284 	void getResendActivation(string _error = "")
285 	{
286 		string error = _error;
287 		render!("userman.resend_activation.dt", error);
288 	}
289 
290 	@errorDisplay!getResendActivation
291 	void postResendActivation(ValidEmail email)
292 	{
293 		try {
294 			m_controller.resendActivation(email);
295 			render!("userman.resend_activation_done.dt");
296 		} catch (Exception e) {
297 			logDebug("Error sending activation mail: %s", e.toString().sanitize);
298 			throw new Exception("Failed to send activation mail. Please try again later. ("~e.msg~").");
299 		}
300 	}
301 
302 	void getActivate(ValidEmail email, string code)
303 	{
304 		m_controller.activateUser(email, code);
305 		auto user = m_controller.getUserByEmail(email);
306 		m_sessUserEmail = user.email;
307 		m_sessUserName = user.name;
308 		m_sessUserFullName = user.fullName;
309 		m_sessUserID = user._id.toString();
310 		render!("userman.activate.dt");
311 	}
312 	
313 	void getForgotLogin(string _error = "")
314 	{
315 		auto error = _error;
316 		render!("userman.forgot_login.dt", error);
317 	}
318 
319 	@errorDisplay!getForgotLogin
320 	void postForgotLogin(ValidEmail email)
321 	{
322 		try {
323 			m_controller.requestPasswordReset(email);
324 		} catch(Exception e) {
325 			// ignore errors, so that registered e-mails cannot be determined
326 			logDiagnostic("Failed to send password reset mail to %s: %s", email, e.msg);
327 		}
328 
329 		render!("userman.forgot_login_sent.dt");
330 	}
331 
332 	void getResetPassword(string _error = "")
333 	{
334 		string error = _error;
335 		render!("userman.reset_password.dt", error);
336 	}
337 
338 	@errorDisplay!getResetPassword
339 	void postResetPassword(ValidEmail email, string code, ValidPassword password, Confirm!"password" password_confirmation, HTTPServerResponse res)
340 	{
341 		m_controller.resetPassword(email, code, password);
342 		res.headers["Refresh"] = "3; url=" ~ m_controller.settings.serviceUrl.toString();
343 		render!("userman.reset_password_done.dt");
344 	}
345 
346 	@auth
347 	void getProfile(HTTPServerRequest req, User _user, string _error = "")
348 	{
349 		req.form["full_name"] = _user.fullName;
350 		req.form["email"] = _user.email;
351 		bool useUserNames = m_controller.settings.useUserNames;
352 		auto user = _user;
353 		string error = _error;
354 		render!("userman.profile.dt", user, useUserNames, error);
355 	}
356 	
357 	@auth @errorDisplay!getProfile
358 	void postProfile(HTTPServerRequest req, User _user)
359 	{
360 		updateProfile(m_controller, _user, req);
361 		redirect(m_prefix);
362 	}
363 
364 	// Attribute for authenticated routes
365 	private enum auth = before!performAuth("_user");
366 	mixin PrivateAccessProxy;
367 
368 	private User performAuth(HTTPServerRequest req, HTTPServerResponse res)
369 	{
370 		return m_auth.performAuth(req, res);
371 	}
372 }