1 /**
2 	Web admin interface implementation
3 
4 	Copyright: © 2015-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.webadmin;
9 
10 public import userman.api;
11 import userman.userman : validateUserName;
12 
13 import vibe.core.log;
14 import vibe.http.router;
15 import vibe.textfilter.urlencode;
16 import vibe.utils.validation;
17 import vibe.web.auth;
18 import vibe.web.web;
19 
20 import std.algorithm : min, max;
21 import std.array : appender;
22 import std.conv : to;
23 import std.exception;
24 import std.typecons : Nullable;
25 
26 
27 /**
28 	Registers the routes for a UserMan web admin interface.
29 */
30 void registerUserManWebAdmin(URLRouter router, UserManAPI api)
31 {
32 	router.registerWebInterface(new UserManWebAdminInterface(api));
33 }
34 
35 /// private
36 @requiresAuth
37 @translationContext!TranslationContext
38 class UserManWebAdminInterface {
39 	enum adminGroupName = "userman.admins";
40 
41 	private {
42 		UserManAPI m_api;
43 		int m_entriesPerPage = 50;
44 		SessionVar!(User.ID, "authUser") m_authUser;
45 		SessionVar!(string, "authUserDisplayName") m_authUserDisplayName;
46 	}
47 
48 	this(UserManAPI api)
49 	{
50 		m_api = api;
51 	}
52 
53 	@noAuth
54 	void getLogin(string redirect = "/", string _error = null)
55 	{
56 		bool first_user = m_api.users.count == 0;
57 		string error = _error;
58 		render!("userman.admin.login.dt", first_user, error, redirect);
59 	}
60 
61 	@noAuth @errorDisplay!getLogin
62 	void postLogin(string name, string password, string redirect = "/")
63 	{
64 		import std.algorithm.searching : canFind;
65 
66 		User.ID uid;
67 		try uid = m_api.users.testLogin(name, password);
68 		catch (Exception e) {
69 			import std.encoding : sanitize;
70 			logDebug("Error logging in: %s", e.toString().sanitize);
71 			throw new Exception("Invalid user/email or password.");
72 		}
73 
74 		auto user = m_api.users[uid].get();
75 		enforce(user.active, "The account is not yet activated.");
76 		enforce(m_api.users[uid].getGroups().canFind(adminGroupName), "User is not an administrator.");
77 
78 		m_authUser = user.id;
79 		m_authUserDisplayName = user.fullName;
80 		.redirect(redirect);
81 	}
82 
83 	@noAuth @errorDisplay!getLogin
84 	void postInitialRegister(string username, ValidEmail email, string full_name, ValidPassword password, Confirm!"password" password_confirmation, string redirect = "/")
85 	{
86 		auto err = appender!string();
87 		enforceHTTP(m_api.settings.userNameSettings.validateUserName(err, username), HTTPStatus.badRequest, err.data);
88 
89 		enforceHTTP(m_api.users.count == 0, HTTPStatus.forbidden, "Cannot create initial admin account when other accounts already exist.");
90 		try m_api.groups[adminGroupName].get();
91 		catch (Exception) m_api.groups.create(adminGroupName, "UserMan Administrators");
92 
93 		auto uid = m_api.users.register(email, username, full_name, password);
94 		m_api.groups[adminGroupName].members.add(uid);
95 		m_authUser = uid;
96 		.redirect(redirect);
97 	}
98 
99 	@noAuth void getLogout()
100 	{
101 		terminateSession();
102 		redirect("/");
103 	}
104 
105 
106 	// everything below requires authentication
107 	@anyAuth:
108 
109 	void get(AuthInfo auth)
110 	{
111 		render!("userman.admin.index.dt");
112 	}
113 
114 	/*********/
115 	/* Users */
116 	/**************************************************************************/
117 
118 	void getUsers(AuthInfo auth, int page = 1, string _error = null)
119 	{
120 		static struct Info {
121 			User[] users;
122 			int pageCount;
123 			int page;
124 			string error;
125 		}
126 
127 		Info info;
128 		info.page = page;
129 		info.pageCount = ((m_api.users.count + m_entriesPerPage - 1) / m_entriesPerPage).to!int;
130 		info.users = m_api.users.getRange((page-1) * m_entriesPerPage, m_entriesPerPage);
131 		info.error = _error;
132 		render!("userman.admin.users.dt", info);
133 	}
134 
135 	@errorDisplay!getUsers
136 	void postUsers(AuthInfo auth, string name, ValidEmail email, string full_name, ValidPassword password, Confirm!"password" password_confirmation)
137 	{
138 		auto err = appender!string();
139 		enforceHTTP(m_api.settings.userNameSettings.validateUserName(err, name), HTTPStatus.badRequest, err.data);
140 
141 		m_api.users.register(email, name, full_name, password);
142 		redirect("users");
143 	}
144 
145 	@path("/users/multi") @errorDisplay!getUsers
146 	void postMultiUserUpdate(AuthInfo auth, string action, HTTPServerRequest req, /*User.ID[] selection,*/ int page = 1)
147 	{
148 		import std.algorithm : map;
149 		foreach (u; /*selection*/req.form.getAll("selection").map!(id => User.ID.fromString(id)))
150 			performUserAction(u, action);
151 		redirect(page > 1 ? "/users?page="~page.to!string : "/users");
152 	}
153 
154 	@path("/users/:user/")
155 	void getUser(AuthInfo auth, User.ID _user, string _error = null)
156 	{
157 		import vibe.data.json : Json;
158 
159 		static struct Info {
160 			User user;
161 			Json[string] userProperties;
162 			string error;
163 		}
164 		Info info;
165 		info.user = m_api.users[_user].get();
166 		info.userProperties = m_api.users[_user].properties.get();
167 		info.error = _error;
168 		render!("userman.admin.user.dt", info);
169 	}
170 
171 	@path("/users/:user/") @errorDisplay!getUser
172 	void postUser(AuthInfo auth, User.ID _user, string username, ValidEmail email, string full_name, bool active, bool banned)
173 	{
174 		auto err = appender!string();
175 		enforceHTTP(m_api.settings.userNameSettings.validateUserName(err, username), HTTPStatus.badRequest, err.data);
176 
177 		//m_api.users[_user].setName(username); // TODO!
178 		m_api.users[_user].setEmail(email);
179 		m_api.users[_user].setFullName(full_name);
180 		m_api.users[_user].setActive(active);
181 		m_api.users[_user].setBanned(banned);
182 		redirect("/users/"~_user.toString~"/");
183 	}
184 
185 	@path("/users/:user/password") @errorDisplay!getUser
186 	void postUserPassword(AuthInfo auth, User.ID _user, ValidPassword password, Confirm!"password" password_confirmation)
187 	{
188 		m_api.users[_user].setPassword(password);
189 		redirect("/users/"~_user.toString~"/");
190 	}
191 
192 	@path("/users/:user/set_property") @errorDisplay!getUser
193 	void postSetUserProperty(AuthInfo auth, User.ID _user, Nullable!string old_name, string name, string value)
194 	{
195 		import vibe.data.json : parseJson;
196 
197 		if (!old_name.isNull() && old_name != name)
198 			m_api.users[_user].properties[old_name].remove();
199 		if (name.length) m_api.users[_user].properties[name].set(parseJson(value));
200 		redirect("./");
201 	}
202 
203 	/**********/
204 	/* Groups */
205 	/**************************************************************************/
206 
207 	void getGroups(AuthInfo auth, long page = 1, string _error = null)
208 	{
209 		static struct Info {
210 			Group[] groups;
211 			long pageCount;
212 			long page;
213 			string error;
214 		}
215 
216 		Info info;
217 		info.page = page;
218 		info.pageCount = (m_api.groups.count + m_entriesPerPage - 1) / m_entriesPerPage;
219 		info.groups = m_api.groups.getRange((page-1) * m_entriesPerPage, m_entriesPerPage);
220 		info.error = _error;
221 		render!("userman.admin.groups.dt", info);
222 	}
223 
224 	@errorDisplay!getGroups
225 	void postGroups(AuthInfo auth, ValidGroupName name, string description)
226 	{
227 		m_api.groups.create(name, description);
228 		redirect("/groups/"~name~"/");
229 	}
230 
231 	@path("/groups/multi") @errorDisplay!getGroups
232 	void postMultiGroupUpdate(AuthInfo auth, string action, HTTPServerRequest req, /*User.ID[] selection,*/ int page = 1)
233 	{
234 		import std.algorithm : map;
235 		foreach (g; /*selection*/req.form.getAll("selection"))
236 			performGroupAction(g, action);
237 		redirect(page > 1 ? "/groups?page="~page.to!string : "/groups");
238 	}
239 
240 	@path("/groups/:group/")
241 	void getGroup(AuthInfo auth, string _group, string _error = null)
242 	{
243 		static struct Info {
244 			Group group;
245 			long memberCount;
246 			string error;
247 		}
248 		Info info;
249 		info.group = m_api.groups[_group].get();
250 		info.memberCount = m_api.groups[_group].members.count();
251 		info.error = _error;
252 		render!("userman.admin.group.dt", info);
253 	}
254 
255 	@path("/groups/:group/") @errorDisplay!getGroup
256 	void postGroup(AuthInfo auth, string _group, string description)
257 	{
258 		m_api.groups[_group].setDescription(description);
259 		redirect("/groups/"~_group~"/");
260 	}
261 
262 	/*@auth @path("/group/:group/set_property") @errorDisplay!getGroup
263 	void postSetGroupProperty(AuthInfo auth, string _group, Nullable!string old_name, string name, string value)
264 	{
265 		import vibe.data.json : parseJson;
266 
267 		if (!old_name.isNull() && old_name != name)
268 			m_api.groups.removeProperty(_group, old_name);
269 		if (name.length) m_api.groups.setProperty(_group, name, parseJson(value));
270 		redirect("./");
271 	}*/
272 
273 	@path("/groups/:group/members/")
274 	void getGroupMembers(AuthInfo auth, string _group, long page = 1, string _error = null)
275 	{
276 		import std.algorithm : map;
277 		import std.array : array;
278 
279 		static struct Info {
280 			Group group;
281 			User[] members;
282 			long page;
283 			long pageCount;
284 			string error;
285 		}
286 		Info info;
287 		info.group = m_api.groups[_group].get();
288 		info.page = page;
289 		info.pageCount = ((m_api.groups.count + m_entriesPerPage - 1) / m_entriesPerPage).to!int;
290 		info.members = m_api.groups[_group].members.getRange((page-1) * m_entriesPerPage, m_entriesPerPage)
291 			.map!(id => m_api.users[id].get())
292 			.array;
293 		info.error = _error;
294 		render!("userman.admin.group.members.dt", info);
295 	}
296 
297 	@path("/groups/:group/members/:user/remove") @errorDisplay!getGroupMembers
298 	void postRemoveMember(AuthInfo auth, string _group, User.ID _user)
299 	{
300 		enforce(_group != adminGroupName || _user != auth.user.id,
301 			"Cannot remove yourself from the admin group.");
302 		m_api.groups[_group].members[_user].remove();
303 		redirect("/groups/"~_group~"/members/");
304 	}
305 
306 	@path("/groups/:group/members/") @errorDisplay!getGroupMembers
307 	void postAddMember(AuthInfo auth, string _group, string username)
308 	{
309 		auto uid = m_api.users.getByName(username).id;
310 		m_api.groups[_group].members.add(uid);
311 		redirect("/groups/"~_group~"/members/");
312 	}
313 
314 	/************/
315 	/* Settings */
316 	/**************************************************************************/
317 
318 	@path("/settings/")
319 	void getSettings(AuthInfo auth, string _error = null)
320 	{
321 		struct Info {
322 			string error;
323 			UserManAPISettings settings;
324 		}
325 
326 		Info info;
327 		info.error = _error;
328 		info.settings = m_api.settings;
329 		render!("userman.admin.settings.dt", info);
330 	}
331 
332 	@path("/settings/") @errorDisplay!getSettings
333 	void posttSettings(AuthInfo auth)
334 	{
335 		// TODO!
336 		redirect("/settings/");
337 	}
338 
339 	private void performUserAction(User.ID user, string action)
340 	{
341 		switch (action) {
342 			default: throw new Exception("Unknown action: "~action);
343 			case "activate": m_api.users[user].setActive(true); break;
344 			case "deactivate": m_api.users[user].setActive(false); break;
345 			case "ban": m_api.users[user].setBanned(true); break;
346 			case "unban": m_api.users[user].setBanned(false); break;
347 			case "delete": m_api.users[user].remove(); break;
348 			case "sendActivation":
349 				auto email = m_api.users[user].get().email;
350 				m_api.users.resendActivation(email);
351 				break;
352 		}
353 	}
354 
355 	private void performGroupAction(string group, string action)
356 	{
357 		switch (action) {
358 			default: throw new Exception("Unknown action: "~action);
359 			case "delete":
360 				enforce(group != adminGroupName, "Cannot remove admin group.");
361 				m_api.groups[group].remove();
362 				break;
363 		}
364 	}
365 
366 	@noRoute AuthInfo authenticate(HTTPServerRequest req, HTTPServerResponse res)
367 	@trusted {
368 		if (m_authUser == User.ID.init) {
369 			redirect("/login?redirect="~req.path.urlEncode);
370 			return AuthInfo.init;
371 		} else {
372 			return AuthInfo(m_api.users[m_authUser].get());
373 		}
374 	}
375 }
376 
377 private struct AuthInfo {
378 	User user;
379 }
380 
381 private struct TranslationContext {
382 	import std.meta : AliasSeq;
383 	alias languages = AliasSeq!("en_US", "de_DE");
384 	//mixin translationModule!"userman";
385 }
386 
387 struct ValidGroupName {
388 	string m_value;
389 
390 	@disable this();
391 
392 	private this(string value) { m_value = value; }
393 
394 	static Nullable!ValidGroupName fromStringValidate(string str, string* err)
395 	{
396 		import vibe.utils.validation : validateIdent;
397 		import std.algorithm : splitter;
398 		import std.array : appender;
399 
400 		// work around disabled default construction
401 		auto ret = Nullable!ValidGroupName(ValidGroupName(null));
402 		ret.nullify();
403 
404 		if (str.length < 1) {
405 			*err = "Group names must not be empty.";
406 			return ret;
407 		}
408 		auto errapp = appender!string;
409 		foreach (p; str.splitter(".")) {
410 			if (!validateIdent(errapp, p)) {
411 				*err = errapp.data;
412 				return ret;
413 			}
414 		}
415 
416 		ret = ValidGroupName(str);
417 		return ret;
418 	}
419 
420 	string toString() const { return m_value; }
421 
422 	alias toString this;
423 }