1 /**
2 	(module summary)
3 
4 	Copyright: © 2012-2016 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 vibenews.admin;
9 
10 import vibenews.controller;
11 import vibenews.vibenews;
12 
13 import userman.db.controller : User;
14 static import userman.db.controller;
15 
16 import vibe.core.log;
17 import vibe.crypto.passwordhash;
18 import vibe.data.bson;
19 import vibe.http.router;
20 import vibe.http.server;
21 import vibe.http.fileserver;
22 import vibe.textfilter.urlencode;
23 import vibe.utils.validation;
24 
25 import std.algorithm : map;
26 import std.array;
27 import std.conv;
28 import std.exception;
29 import std.string;
30 import std.variant;
31 
32 class AdminInterface {
33 	private {
34 		Controller m_ctrl;
35 	}
36 
37 	this(Controller ctrl)
38 	{
39 		m_ctrl = ctrl;
40 	}
41 
42 	void listen()
43 	{
44 		auto vnsettings = m_ctrl.settings;
45 
46 		auto settings = new HTTPServerSettings;
47 		settings.port = vnsettings.adminPort;
48 		settings.bindAddresses = vnsettings.adminBindAddresses;
49 
50 		auto router = new URLRouter;
51 		register(router);
52 
53 		listenHTTP(settings, router);
54 	}
55 
56 	void register(URLRouter router)
57 	{
58 		router.get("/", &showAdminPanel);
59 		router.post("/categories/create", &createGroupCategory);
60 		router.get("/categories/:category/show", &showGroupCategory);
61 		router.post("/categories/:category/update", &updateGroupCategory);
62 		router.post("/categories/:category/delete", &deleteGroupCategory);
63 		router.post("/reclassify_spam", &reclassifySpam);
64 		router.post("/groups/create", &createGroup);
65 		router.post("/groups/repair-numbers", &repairGroupNumbers);
66 		router.post("/groups/repair-threads", &repairGroupThreads);
67 		router.get("/groups/:groupname/show", &showGroup);
68 		router.post("/groups/:groupname/update", &updateGroup);
69 		router.post("/groups/:groupname/purge", &purgeGroup);
70 		router.get("/groups/:groupname/articles", &showArticles);
71 		router.post("/articles/:articleid/activate", &activateArticle);
72 		router.post("/articles/:articleid/deactivate", &deactivateArticle);
73 		router.post("/articles/:articleid/mark_ham", &markAsHam);
74 		router.post("/articles/:articleid/mark_spam", &markAsSpam);
75 		router.get("/users/", &showListUsers);
76 		router.get("/users/:user/", &showUser);
77 		router.post("/users/:user/update", &updateUser);
78 		router.post("/users/:user/delete", &deleteUser);
79 		router.get("*", serveStaticFiles("public"));
80 	}
81 
82 	void showAdminPanel(HTTPServerRequest req, HTTPServerResponse res)
83 	{
84 		struct Info {
85 			VibeNewsSettings settings;
86 		}
87 		auto info = Info(m_ctrl.settings);
88 
89 		Group[] groups;
90 		GroupCategory[] categories;
91 		m_ctrl.enumerateGroups((idx, group){ groups ~= group; }, true);
92 		m_ctrl.enumerateGroupCategories((idx, cat){ categories ~= cat; });
93 		res.render!("vibenews.admin.index.dt", req, info, groups, categories);
94 	}
95 
96 	void showGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
97 	{
98 		struct Info {
99 			VibeNewsSettings settings;
100 		}
101 		auto info = Info(m_ctrl.settings);
102 
103 		auto category = m_ctrl.getGroupCategory(BsonObjectID.fromString(req.params["category"]));
104 		Group[] groups;
105 		m_ctrl.enumerateGroups((idx, grp){ groups ~= grp; });
106 		res.render!("vibenews.admin.editcategory.dt", req, info, category, groups);
107 	}
108 
109 	void updateGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
110 	{
111 		auto id = BsonObjectID.fromString(req.params["category"]);
112 		auto caption = req.form["caption"];
113 		auto index = req.form["index"].to!int();
114 		BsonObjectID[] groups;
115 		m_ctrl.enumerateGroups((idx, grp){ if( grp._id.toString() in req.form ) groups ~= grp._id; });
116 		m_ctrl.updateGroupCategory(id, caption, index, groups);
117 		res.redirect("/categories/"~id.toString()~"/show");
118 	}
119 
120 	void deleteGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
121 	{
122 		auto id = BsonObjectID.fromString(req.params["category"]);
123 		m_ctrl.deleteGroupCategory(id);
124 		res.redirect("/");
125 	}
126 
127 	void showGroup(HTTPServerRequest req, HTTPServerResponse res)
128 	{
129 		struct Info {
130 			VibeNewsSettings settings;
131 		}
132 		auto info = Info(m_ctrl.settings);
133 
134 		auto group = m_ctrl.getGroupByName(req.params["groupname"], true);
135 		res.render!("vibenews.admin.editgroup.dt", req, info, group);
136 	}
137 
138 	void createGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
139 	{
140 		auto id = m_ctrl.createGroupCategory(req.form["caption"], req.form["index"].to!int());
141 		res.redirect("/categories/"~id.toString()~"/show");
142 	}
143 
144 	void reclassifySpam(HTTPServerRequest req, HTTPServerResponse res)
145 	{
146 		m_ctrl.reclassifySpam();
147 		res.redirect("/");
148 	}
149 
150 	void createGroup(HTTPServerRequest req, HTTPServerResponse res)
151 	{
152 		enforce(!m_ctrl.groupExists(req.form["name"], true), "A group with the specified name already exists");
153 	
154 		Group group;
155 		group._id = BsonObjectID.generate();
156 		group.active = false;
157 		group.name = req.form["name"];
158 		group.caption = req.form["caption"];
159 		m_ctrl.addGroup(group);
160 
161 		res.redirect("/groups/"~urlEncode(group.name)~"/show");
162 	}
163 
164 	void updateGroup(HTTPServerRequest req, HTTPServerResponse res)
165 	{
166 		auto group = m_ctrl.getGroupByName(req.params["groupname"], true);
167 		group.caption = req.form["caption"];
168 		group.description = req.form["description"];
169 		group.active = ("active" in req.form) !is null;
170 		group.readOnlyAuthTags = req.form["roauthtags"].split(",").map!(s => authGroupPrefix ~ strip(s))().array();
171 		group.readWriteAuthTags = req.form["rwauthtags"].split(",").map!(s => authGroupPrefix ~ strip(s))().array();
172 		m_ctrl.updateGroup(group);
173 		res.redirect("/groups/"~urlEncode(group.name)~"/show");
174 	}
175 
176 	void purgeGroup(HTTPServerRequest req, HTTPServerResponse res)
177 	{
178 		m_ctrl.purgeGroup(req.params["groupname"]);
179 		res.redirect("/groups/"~req.params["groupname"]~"/show");
180 	}
181 
182 	void repairGroupNumbers(HTTPServerRequest req, HTTPServerResponse res)
183 	{
184 		m_ctrl.repairGroupNumbers();
185 		res.redirect("/");
186 	}
187 
188 	void repairGroupThreads(HTTPServerRequest req, HTTPServerResponse res)
189 	{
190 		m_ctrl.repairThreads();
191 		res.redirect("/");
192 	}
193 
194 	void showArticles(HTTPServerRequest req, HTTPServerResponse res)
195 	{
196 		struct Info {
197 			VibeNewsSettings settings;
198 			enum articlesPerPage = 20;
199 			string groupname;
200 			int page;
201 			Article[] articles;
202 			int articleCount;
203 			int pageCount;
204 		}
205 		Info info;
206 		info.settings = m_ctrl.settings;
207 		info.groupname = req.params["groupname"];
208 		info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0;
209 		m_ctrl.enumerateAllArticlesBackwards(info.groupname, info.page*info.articlesPerPage, info.articlesPerPage, (ref art){ info.articles ~= art; });
210 		info.articleCount = cast(int)m_ctrl.getAllArticlesCount(info.groupname);
211 		info.pageCount = (info.articleCount-1)/info.articlesPerPage + 1;
212 
213 		res.render!("vibenews.admin.listarticles.dt", req, info);
214 	}
215 
216 	void activateArticle(HTTPServerRequest req, HTTPServerResponse res)
217 	{
218 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
219 		m_ctrl.activateArticle(artid);
220 		res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]);
221 	}
222 
223 	void deactivateArticle(HTTPServerRequest req, HTTPServerResponse res)
224 	{
225 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
226 		m_ctrl.deactivateArticle(artid);
227 		res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]);
228 	}
229 
230 	void markAsSpam(HTTPServerRequest req, HTTPServerResponse res)
231 	{
232 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
233 		m_ctrl.markAsSpam(artid, true);
234 		res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]);
235 	}
236 
237 	void markAsHam(HTTPServerRequest req, HTTPServerResponse res)
238 	{
239 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
240 		m_ctrl.markAsSpam(artid, false);
241 		res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]);
242 	}
243 
244 	void showListUsers(HTTPServerRequest req, HTTPServerResponse res)
245 	{
246 		struct Info {
247 			VibeNewsSettings settings;
248 			enum itemsPerPage = 20;
249 			UserInfo[] users;
250 			int page;
251 			int itemCount;
252 			int pageCount;
253 		}
254 
255 		Info info;
256 		info.settings = m_ctrl.settings;
257 		info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0;
258 		string[userman.db.controller.Group.ID] groups;
259 		m_ctrl.enumerateUsers(info.page*info.itemsPerPage, info.itemsPerPage, (ref user){
260 			info.users ~= getUserInfo(m_ctrl, user, groups);
261 		});
262 		info.itemCount = cast(int)m_ctrl.getUserCount();
263 		info.pageCount = (info.itemCount-1)/info.itemsPerPage + 1;
264 
265 		res.render!("vibenews.admin.listusers.dt", req, info);
266 	}
267 
268 	void showUser(HTTPServerRequest req, HTTPServerResponse res)
269 	{
270 		struct Info {
271 			VibeNewsSettings settings;
272 			UserInfo user;
273 		}
274 		User usr = m_ctrl.getUser(User.ID.fromString(req.params["user"]));
275 		string[userman.db.controller.Group.ID] groups;
276 		Info info;
277 		info.settings = m_ctrl.settings;
278 		info.user = getUserInfo(m_ctrl, usr, groups);
279 		res.render!("vibenews.admin.edituser.dt", req, info);
280 	}
281 
282 	void updateUser(HTTPServerRequest req, HTTPServerResponse res)
283 	{
284 		import std.algorithm.iteration : splitter;
285 
286 		auto user = m_ctrl.getUser(User.ID.fromString(req.params["user"]));
287 		if (auto pv = "email" in req.form) {
288 			validateEmail(*pv);
289 			user.email = user.name = *pv;
290 		}
291 		if (auto pv = "fullName" in req.form) user.fullName = *pv;
292 		if (auto pv = "groups" in req.form) {
293 			user.groups.length = 0;
294 			foreach (grp; (*pv).splitter(",").map!(g => authGroupPrefix ~ g.strip())) {
295 				try user.groups ~= m_ctrl.getAuthGroupByName(grp).id;
296 				catch (Exception) {
297 					m_ctrl.userManController.addGroup(grp, "VibeNews authentication group");
298 					user.groups ~= m_ctrl.getAuthGroupByName(grp).id;
299 				}
300 			}
301 		}
302 		user.active = ("active" in req.form) !is null;
303 		user.banned = ("banned" in req.form) !is null;
304 		m_ctrl.updateUser(user);
305 
306 		res.redirect("/users/");
307 	}
308 
309 	void deleteUser(HTTPServerRequest req, HTTPServerResponse res)
310 	{
311 		m_ctrl.deleteUser(User.ID.fromString(req.params["user"]));
312 		res.redirect("/users/");
313 	}
314 }
315 
316 struct UserInfo {
317 	User user;
318 	alias user this;
319 	ulong messageCount;
320 	ulong deletedMessageCount;
321 	string[] groupStrings;
322 }
323 
324 private UserInfo getUserInfo(Controller ctrl, User user, ref string[userman.db.controller.Group.ID] groups)
325 {
326 	UserInfo nfo;
327 	nfo.user = user;
328 	ctrl.getUserMessageCount(user.email, nfo.messageCount, nfo.deletedMessageCount);
329 	foreach (g; user.groups) {
330 		string grpname;
331 		if (auto gd = g in groups) grpname = *gd;
332 		else grpname = groups[g] = ctrl.getAuthGroup(g).name;
333 		if (!grpname.startsWith(authGroupPrefix)) continue;
334 		grpname = grpname[authGroupPrefix.length .. $];
335 		nfo.groupStrings ~= grpname;
336 	}
337 	return nfo;
338 }