1 /**
2 	Web interface implementation
3 
4 	Copyright: © 2012-2017 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.api;
11 import userman.db.controller : UserManController;
12 import userman.userman : validateUserName;
13 
14 import vibe.core.log;
15 import vibe.http.router;
16 import vibe.textfilter.urlencode;
17 import vibe.utils.validation;
18 import vibe.web.auth;
19 import vibe.web.web;
20 
21 import std.array : appender;
22 import std.exception;
23 import std.typecons : Nullable;
24 
25 
26 /**
27 	Registers the routes for a UserMan web interface.
28 
29 	Use this to add user management to your web application. See also
30 	$(D UserManWebAuthenticator) for some complete examples of a simple
31 	web service with UserMan integration.
32 */
33 void registerUserManWebInterface(URLRouter router, UserManAPI api)
34 {
35 	router.registerWebInterface(new UserManWebInterface(api));
36 }
37 /// deprecated
38 void registerUserManWebInterface(URLRouter router, UserManController controller)
39 {
40 	router.registerUserManWebInterface(createLocalUserManAPI(controller));
41 }
42 
43 
44 /**
45 	Helper function to update the user profile from a POST request.
46 
47 	This assumes that the fields are named like they are in userman.profile.dt.
48 	Session variables will be updated automatically.
49 */
50 void updateProfile(UserManAPI api, User.ID user, HTTPServerRequest req)
51 {
52 	/*if (api.settings.useUserNames) {
53 		if (auto pv = "name" in req.form) {
54 			api.users.setName(user, *pv);
55 			req.session.set("userName", *pv);
56 		}
57 	}*/ // TODO!
58 	if (auto pv = "email" in req.form) {
59 		api.users[user].setEmail(*pv);
60 		req.session.set("userEmail", *pv);
61 	}
62 	if (auto pv = "full_name" in req.form) {
63 		api.users[user].setFullName(*pv);
64 		req.session.set("userFullName", *pv);
65 	}
66 	if (auto pv = "password" in req.form) {
67 		auto pconf = "password_confirmation" in req.form;
68 		enforce(pconf !is null, "Missing password confirmation.");
69 		validatePassword(*pv, *pconf);
70 		api.users[user].setPassword(*pv);
71 	}
72 }
73 /// ditto
74 deprecated void updateProfile(UserManController controller, User user, HTTPServerRequest req)
75 {
76 	updateProfile(createLocalUserManAPI(controller), user.id, req);
77 }
78 
79 
80 /**
81 	Used to provide request authentication for web applications.
82 
83 	Note that it is generally recommended to use the `vibe.web.auth` mechanism
84 	together with `authenticate` instead of using this class.
85 
86 	See_also: `authenticate`
87 */
88 class UserManWebAuthenticator {
89 	private {
90 		UserManAPI m_api;
91 		string m_prefix;
92 	}
93 
94 	this(UserManAPI api, string prefix = "/")
95 	{
96 		m_api = api;
97 		m_prefix = prefix;
98 	}
99 
100 	deprecated this(UserManController controller, string prefix = "/")
101 	{
102 		this(createLocalUserManAPI(controller), prefix);
103 	}
104 
105 	HTTPServerRequestDelegate auth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback)
106 	{
107 		void requestHandler(HTTPServerRequest req, HTTPServerResponse res)
108 		@trusted {
109 			User usr;
110 			try usr = performAuth(req, res);
111 			catch (Exception e) throw new HTTPStatusException(HTTPStatus.unauthorized);
112 			if (res.headerWritten) return;
113 			callback(req, res, usr);
114 		}
115 
116 		return &requestHandler;
117 	}
118 	HTTPServerRequestDelegate auth(HTTPServerRequestDelegate callback)
119 	{
120 		return auth((req, res, user){ callback(req, res); });
121 	}
122 
123 	User performAuth(HTTPServerRequest req, HTTPServerResponse res)
124 	{
125 		if (!req.session) {
126 			res.redirect(m_prefix~"login?redirect="~urlEncode(req.path));
127 			return User.init;
128 		} else {
129 			return m_api.users.getByName(req.session.get!string("userName"));
130 		}
131 	}
132 
133 	HTTPServerRequestDelegate ifAuth(void delegate(HTTPServerRequest, HTTPServerResponse, User) callback)
134 	{
135 		void requestHandler(HTTPServerRequest req, HTTPServerResponse res)
136 		@trusted {
137 			if( !req.session ) return;
138 			auto usr = m_api.users.getByName(req.session.get!string("userName"));
139 			callback(req, res, usr);
140 		}
141 
142 		return &requestHandler;
143 	}
144 }
145 
146 /** An example using a plain $(D vibe.http.router.URLRouter) based
147 	authentication approach.
148 */
149 unittest {
150 	import std.functional; // toDelegate
151 	import vibe.http.router;
152 	import vibe.http.server;
153 
154 	void getIndex(HTTPServerRequest req, HTTPServerResponse res)
155 	{
156 		//render!"welcome.dt"
157 	}
158 
159 	void getPrivatePage(HTTPServerRequest req, HTTPServerResponse res, User user)
160 	{
161 		// render a private page with some user specific information
162 		//render!("private_page.dt", _user);
163 	}
164 
165 	void registerMyService(URLRouter router, UserManAPI userman)
166 	{
167 		auto authenticator = new UserManWebAuthenticator(userman);
168 		router.registerUserManWebInterface(userman);
169 		router.get("/", &getIndex);
170 		router.any("/private_page", authenticator.auth(toDelegate(&getPrivatePage)));
171 	}
172 }
173 
174 
175 /** Ensures that a user is logged in.
176 
177 	If a valid session exists, the returned `User` object will contain all
178 	information about the logged in user. Otherwise, the response object will
179 	be used to redirect the user to the login page and an empty user object
180 	is returned.
181 
182 	Params:
183 		req = The request object of the incoming request
184 		res = The response object of the incoming request
185 		api = A reference to the UserMan API
186 		url_prefix = Optional prefix to prepend to the login page path
187 */
188 User authenticate(HTTPServerRequest req, HTTPServerResponse res, UserManAPI api, string url_prefix = "")
189 @trusted {
190 	if (!req.session) {
191 		res.redirect(url_prefix~"login?redirect="~urlEncode(req.path));
192 		return User.init;
193 	} else {
194 		return api.users.getByName(req.session.get!string("userName"));
195 	}
196 }
197 
198 /** This example uses the $(D @before) annotation supported by the
199 	$(D vibe.web.web) framework for a concise and statically defined
200 	authentication approach.
201 */
202 unittest {
203 	import vibe.http.router;
204 	import vibe.http.server;
205 	import vibe.web.web;
206 
207 	@requiresAuth
208 	class MyWebService {
209 		@safe:
210 		private {
211 			UserManAPI m_api;
212 		}
213 
214 		this(UserManAPI userman)
215 		{
216 			m_api = userman;
217 		}
218 
219 		// this route can be accessed publicly (/)
220 		@noAuth
221 		void getIndex()
222 		{
223 			//render!"welcome.dt"
224 		}
225 
226 		// the @authenticated attribute (defined below) ensures that this route
227 		// (/private_page) can only ever be accessed if the user is logged in
228 		@anyAuth
229 		void getPrivatePage(User user)
230 		{
231 			// render a private page with some user specific information
232 			//render!("private_page.dt", user);
233 		}
234 
235 		@noRoute User authenticate(HTTPServerRequest req, HTTPServerResponse res)
236 		{
237 			return .authenticate(req, res, m_api);
238 		}
239 	}
240 
241 	void registerMyService(URLRouter router, UserManAPI userman)
242 	{
243 		router.registerUserManWebInterface(userman);
244 		router.registerWebInterface(new MyWebService(userman));
245 	}
246 }
247 
248 
249 /** Web interface class for UserMan, suitable for use with $(D vibe.web.web).
250 
251 	The typical approach is to use $(D registerUserManWebInterface) instead of
252 	directly using this class.
253 */
254 @requiresAuth
255 @translationContext!TranslationContext
256 class UserManWebInterface {
257 	private {
258 		UserManAPI m_api;
259 		string m_prefix;
260 		SessionVar!(string, "userEmail") m_sessUserEmail;
261 		SessionVar!(string, "userName") m_sessUserName;
262 		SessionVar!(string, "userFullName") m_sessUserFullName;
263 		SessionVar!(string, "userID") m_sessUserID;
264 		UserManAPISettings m_settings;
265 	}
266 
267 	this(UserManAPI api, string prefix = "/")
268 	{
269 		m_api = api;
270 		m_settings = api.settings;
271 		m_prefix = prefix;
272 	}
273 
274 	deprecated this(UserManController controller, string prefix = "/")
275 	{
276 		this(createLocalUserManAPI(controller), prefix);
277 	}
278 
279 	@noAuth
280 	void getLogin(string redirect = "", string _error = "")
281 	{
282 		string error = _error;
283 		auto settings = m_settings;
284 		render!("userman.login.dt", error, redirect, settings);
285 	}
286 
287 	@noAuth @errorDisplay!getLogin
288 	void postLogin(string name, string password, string redirect = "")
289 	{
290 		User user;
291 		try {
292 			auto uid = m_api.users.testLogin(name, password);
293 			user = m_api.users[uid].get();
294 		} catch (Exception e) {
295 			import std.encoding : sanitize;
296 			logDebug("Error logging in: %s", e.toString().sanitize);
297 			throw new Exception("Invalid user/email or password.");
298 		}
299 
300 		enforce(user.active, "The account is not yet activated.");
301 
302 		m_sessUserEmail = user.email;
303 		m_sessUserName = user.name;
304 		m_sessUserFullName = user.fullName;
305 		m_sessUserID = user.id.toString();
306 		.redirect(redirect.length ? redirect : m_prefix);
307 	}
308 
309 	@noAuth
310 	void getLogout(HTTPServerResponse res)
311 	{
312 		terminateSession();
313 		res.headers["Refresh"] = "3; url="~m_settings.serviceURL.toString();
314 		render!("userman.logout.dt");
315 	}
316 
317 	@noAuth
318 	void getRegister(string _error = "")
319 	{
320 		string error = _error;
321 		auto settings = m_settings;
322 		render!("userman.register.dt", error, settings);
323 	}
324 
325 	@noAuth @errorDisplay!getRegister
326 	void postRegister(ValidEmail email, Nullable!string name, string fullName, ValidPassword password, Confirm!"password" passwordConfirmation)
327 	{
328 		string username;
329 		if (m_settings.useUserNames) {
330 			enforce(!name.isNull, "Missing user name field.");
331 
332 			auto err = appender!string();
333 			enforce(m_settings.userNameSettings.validateUserName(err, name), err.data);
334 
335 			username = name;
336 		} else username = email;
337 
338 		m_api.users.register(email, username, fullName, password);
339 
340 		if (m_settings.requireActivation) {
341 			string error;
342 			render!("userman.register_activate.dt", error);
343 		} else {
344 			postLogin(username, password);
345 		}
346 	}
347 
348 	@noAuth
349 	void getResendActivation(string _error = "")
350 	{
351 		string error = _error;
352 		render!("userman.resend_activation.dt", error);
353 	}
354 
355 	@noAuth @errorDisplay!getResendActivation
356 	void postResendActivation(ValidEmail email)
357 	{
358 		try {
359 			m_api.users.resendActivation(email);
360 			render!("userman.resend_activation_done.dt");
361 		} catch (Exception e) {
362 			import std.encoding : sanitize;
363 			logDebug("Error sending activation mail: %s", e.toString().sanitize);
364 			throw new Exception("Failed to send activation mail. Please try again later. ("~e.msg~").");
365 		}
366 	}
367 
368 	@noAuth
369 	void getActivate(ValidEmail email, string code)
370 	{
371 		m_api.users.activate(email, code);
372 		auto user = m_api.users.getByEmail(email);
373 		m_sessUserEmail = user.email;
374 		m_sessUserName = user.name;
375 		m_sessUserFullName = user.fullName;
376 		m_sessUserID = user.id.toString();
377 		render!("userman.activate.dt");
378 	}
379 
380 	@noAuth
381 	void getForgotLogin(string _error = "")
382 	{
383 		auto error = _error;
384 		render!("userman.forgot_login.dt", error);
385 	}
386 
387 	@noAuth @errorDisplay!getForgotLogin
388 	void postForgotLogin(ValidEmail email)
389 	{
390 		try {
391 			m_api.users.requestPasswordReset(email);
392 		} catch(Exception e) {
393 			// ignore errors, so that registered e-mails cannot be determined
394 			logDiagnostic("Failed to send password reset mail to %s: %s", email, e.msg);
395 		}
396 
397 		render!("userman.forgot_login_sent.dt");
398 	}
399 
400 	@noAuth
401 	void getResetPassword(string _error = "")
402 	{
403 		string error = _error;
404 		render!("userman.reset_password.dt", error);
405 	}
406 
407 	@noAuth @errorDisplay!getResetPassword
408 	void postResetPassword(ValidEmail email, string code, ValidPassword password, Confirm!"password" password_confirmation, HTTPServerResponse res)
409 	{
410 		m_api.users.resetPassword(email, code, password);
411 		res.headers["Refresh"] = "3; url=" ~ m_settings.serviceURL.toString();
412 		render!("userman.reset_password_done.dt");
413 	}
414 
415 	@anyAuth
416 	void getProfile(HTTPServerRequest req, User _user, string _error = "")
417 	{
418 		req.form["full_name"] = _user.fullName;
419 		req.form["email"] = _user.email;
420 		bool useUserNames = m_settings.useUserNames;
421 		auto user = _user;
422 		string error = _error;
423 		render!("userman.profile.dt", user, useUserNames, error);
424 	}
425 
426 	@anyAuth @errorDisplay!getProfile
427 	void postProfile(HTTPServerRequest req, User _user)
428 	{
429 		updateProfile(m_api, _user.id, req);
430 		redirect(m_prefix);
431 	}
432 
433 	@noRoute User authenticate(HTTPServerRequest req, HTTPServerResponse res)
434 	@safe {
435 		return .authenticate(req, res, m_api, m_prefix);
436 	}
437 }
438 
439 private struct TranslationContext {
440 	import std.meta : AliasSeq;
441 	alias languages = AliasSeq!("en_US", "de_DE");
442 	//mixin translationModule!"userman";
443 }