1 module vibelog.webadmin;
2 
3 public import vibelog.controller;
4 
5 import vibelog.config;
6 import vibelog.post;
7 import vibelog.user;
8 
9 import vibe.http.router;
10 import vibe.web.web;
11 import std.exception : enforce;
12 import std.typecons : Nullable;
13 
14 
15 void registerVibeLogWebAdmin(URLRouter router, VibeLogController controller)
16 {
17 	auto websettings = new WebInterfaceSettings;
18 	websettings.urlPrefix = (controller.settings.siteURL.path ~ controller.settings.adminPrefix).toString();
19 	router.registerWebInterface(new VibeLogWebAdmin(controller), websettings);
20 }
21 
22 /// private
23 /*private*/ final class VibeLogWebAdmin {
24 	private {
25 		VibeLogController m_ctrl;
26 		VibeLogSettings m_settings;
27 		string m_subPath;
28 		string m_config;
29 	}
30 
31 	this(VibeLogController controller)
32 	{
33 		m_ctrl = controller;
34 		m_settings = controller.settings;
35 		m_subPath = (m_settings.siteURL.path ~ m_settings.adminPrefix).toString();
36 	}
37 
38 	void getLogin(string redirect = "")
39 	{
40 		auto info = AdminInfo(AuthInfo.init, m_settings);
41 		render!("vibelog.admin.login.dt", info, redirect);
42 	}
43 
44 	// the whole admin interface needs authentication
45 	@auth:
46 
47 	void get(AuthInfo _auth)
48 	{
49 		auto info = AdminInfo(_auth, m_settings);
50 
51 		render!("vibelog.admin.home.dt", info);
52 	}
53 
54 	//
55 	// Configs
56 	//
57 
58 	@path("configs/")
59 	void getConfigs(AuthInfo _auth)
60 	{
61 		enforceAuth(_auth.loginUser.isConfigAdmin());
62 
63 		auto info = ConfigsInfo(_auth, m_settings);
64 		info.configs =  m_ctrl.db.getAllConfigs();
65 		info.activeConfigName = m_settings.configName;
66 
67 		render!("vibelog.admin.editconfiglist.dt", info);
68 	}
69 
70 	@path("configs/:configname/")
71 	void getConfigEdit(string _configname, AuthInfo _auth)
72 	{
73 		enforceAuth(_auth.loginUser.isConfigAdmin());
74 
75 		auto info = ConfigEditInfo(_auth, m_settings);
76 		info.config = m_ctrl.db.getConfig(_configname);
77 		info.globalConfig = m_ctrl.db.getConfig("global", true);
78 
79 		render!("vibelog.admin.editconfig.dt", info);
80 	}
81 
82 	@path("configs/:configname/")
83 	void postPutConfig(HTTPServerRequest req, string language, string copyrightString, string feedTitle, string feedLink, string feedDescription, string feedImageTitle, string feedImageUrl, string _configname, AuthInfo _auth, string categories = null)
84 	{
85 		import std.string;
86 
87 		enforceAuth(_auth.loginUser.isConfigAdmin());
88 		Config cfg = m_ctrl.db.getConfig(_configname);
89 		if( cfg.name == "global" )
90 			cfg.categories = categories.splitLines();
91 		else {
92 			cfg.categories = null;
93 			foreach (k, v; req.form.byKeyValue) {
94 				if (k.startsWith("category_"))
95 					cfg.categories ~= k[9 .. $];
96 			}
97 		}
98 		cfg.language = language;
99 		cfg.copyrightString = copyrightString;
100 		cfg.feedTitle = feedTitle;
101 		cfg.feedLink = feedLink;
102 		cfg.feedDescription = feedDescription;
103 		cfg.feedImageTitle = feedImageTitle;
104 		cfg.feedImageUrl = feedImageUrl;
105 	
106 		m_ctrl.db.setConfig(cfg);
107 
108 		redirect(m_subPath ~ "configs/");
109 	}
110 
111 	@path("configs/:configname/delete")
112 	void postDeleteConfig(string _configname, AuthInfo _auth)
113 	{
114 		enforceAuth(_auth.loginUser.isConfigAdmin());
115 		m_ctrl.db.deleteConfig(_configname);
116 		redirect(m_subPath ~ "configs/");
117 	}
118 
119 
120 	//
121 	// Users
122 	//
123 
124 	@path("users/")
125 	void getUsers(AuthInfo _auth)
126 	{
127 		auto info = AdminInfo(_auth, m_settings);
128 
129 		render!("vibelog.admin.edituserlist.dt", info);
130 	}
131 
132 	@path("users/:username/")
133 	void getUserEdit(string _username, AuthInfo _auth)
134 	{
135 		auto info = UserEditInfo(_auth, m_settings);
136 
137 		info.globalConfig = m_ctrl.db.getConfig("global", true);
138 		info.user = m_ctrl.db.getUserByName(_username);
139 
140 		render!("vibelog.admin.edituser.dt", info);
141 	}
142 
143 	@path("users/:username/")
144 	void postPutUser(string id, string username, string password, string name, string email, string passwordConfirmation, Nullable!string oldPassword, string _username, HTTPServerRequest req, AuthInfo _auth)
145 	{
146 		import vibelog.internal.passwordhash : generatePasswordHash, validatePasswordHash;
147 		import vibe.data.bson : BsonObjectID;
148 		import std.algorithm.searching : startsWith;
149 
150 		User usr;
151 		if( id.length > 0 ){
152 			enforce(_auth.loginUser.isUserAdmin() || username == _auth.loginUser.username,
153 				"You can only change your own account.");
154 			usr = m_ctrl.db.getUser(BsonObjectID.fromHexString(id));
155 			enforce(usr.username == username, "Cannot change the user name!");
156 		} else {
157 			enforce(_auth.loginUser.isUserAdmin(), "You are not allowed to add users.");
158 			usr = new User;
159 			usr.username = username;
160 			foreach (u; _auth.users)
161 				enforce(u.username != usr.username, "A user with the specified user name already exists!");
162 		}
163 		enforce(password == passwordConfirmation, "Passwords do not match!");
164 
165 		usr.name = name;
166 		usr.email = email;
167 
168 		if (password.length) {
169 			enforce((_auth.loginUser.isUserAdmin() && _auth.loginUser._id != usr._id)
170 				|| validatePasswordHash(usr.password, oldPassword.get),
171 				"Old password does not match.");
172 			usr.password = generatePasswordHash(password);
173 		}
174 
175 		if (_auth.loginUser.isUserAdmin()) {
176 			usr.groups = null;
177 			foreach (k, v; req.form.byKeyValue) {
178 				if (k.startsWith("group_"))
179 					usr.groups ~= k[6 .. $];
180 			}
181 
182 			usr.allowedCategories = null;
183 			foreach (k, v; req.form.byKeyValue) {
184 				if (k.startsWith("category_"))
185 					usr.allowedCategories ~= k[9 .. $];
186 			}
187 		}
188 
189 		if( id.length > 0 ){
190 			m_ctrl.db.modifyUser(usr);
191 		} else {
192 			usr._id = m_ctrl.db.addUser(usr);
193 		}
194 
195 		if (_auth.loginUser.isUserAdmin()) redirect(m_subPath~"users/");
196 		else redirect(m_subPath);
197 	}
198 
199 	@path("users/:username/delete")
200 	void postDeleteUser(string _username, AuthInfo _auth)
201 	{
202 		enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to delete users!");
203 		enforce(_auth.loginUser.username != _username, "Cannot delete the own user account!");
204 		foreach (usr; _auth.users)
205 			if (usr.username == _username) {
206 				m_ctrl.db.deleteUser(usr._id);
207 				redirect(m_subPath ~ "users/");
208 				return;
209 			}
210 		
211 		// fall-through (404)
212 	}
213 
214 	@path("users/")
215 	void postAddUser(string username, AuthInfo _auth)
216 	{
217 		enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to add users!");
218 		if (username !in _auth.users) {
219 			auto u = new User;
220 			u.username = username;
221 			m_ctrl.db.addUser(u);
222 		}
223 		redirect(m_subPath ~ "users/" ~ username ~ "/");
224 	}
225 
226 	//
227 	// Posts
228 	//
229 
230 	@path("posts/")
231 	void getPosts(AuthInfo _auth)
232 	{
233 		auto info = PostsInfo(_auth, m_settings);
234 		m_ctrl.db.getAllPosts(0, (size_t idx, Post post){
235 			if (_auth.loginUser.isPostAdmin() || post.author == _auth.loginUser.username
236 				|| _auth.loginUser.mayPostInCategory(post.category))
237 			{
238 				info.posts ~= post;
239 			}
240 			return true;
241 		});
242 
243 		render!("vibelog.admin.editpostslist.dt", info);
244 	}
245 
246 	@path("make_post")
247 	void getMakePost(AuthInfo _auth, string _error = null)
248 	{
249 		auto info = PostEditInfo(_auth, m_settings);
250 		info.globalConfig = m_ctrl.db.getConfig("global", true);
251 		info.error = _error;
252 
253 		render!("vibelog.admin.editpost.dt", info);
254 	}
255 
256 	@auth @errorDisplay!getMakePost
257 	void postMakePost(bool isPublic, bool commentsAllowed, string author,
258 		string date, string category, string slug, string headerImage, string header, string subHeader,
259 		string summary, string summaryTitle, string content, string filters, AuthInfo _auth)
260 	{
261 		postPutPost(null, isPublic, commentsAllowed, author, date, category, slug, headerImage,
262 			header, subHeader, summary, summaryTitle, content, filters, null, _auth);
263 	}
264 
265 	@path("posts/:postname/")
266 	void getEditPost(string _postname, AuthInfo _auth, string _error = null)
267 	{
268 		auto info = PostEditInfo(_auth, m_settings);
269 		info.globalConfig = m_ctrl.db.getConfig("global", true);
270 		info.post = m_ctrl.db.getPost(_postname);
271 		info.files = m_ctrl.db.getFiles(_postname);
272 		info.error = _error;
273 		render!("vibelog.admin.editpost.dt", info);
274 	}
275 
276 	@path("posts/:postname/delete")
277 	void postDeletePost(string id, string _postname, AuthInfo _auth)
278 	{
279 		import vibe.data.bson : BsonObjectID;
280 		// FIXME: check permissons!
281 		auto bid = BsonObjectID.fromHexString(id);
282 		m_ctrl.db.deletePost(bid);
283 		redirect(m_subPath ~ "posts/");
284 	}
285 
286 	@path("posts/:postname/") @errorDisplay!getEditPost
287 	void postPutPost(string id, bool isPublic, bool commentsAllowed, string author,
288 		string date, string category, string slug, string headerImage, string header, string subHeader,
289 		string summary, string summaryTitle, string content, string filters, string _postname, AuthInfo _auth)
290 	{
291 		import vibe.data.bson : BsonObjectID;
292 
293 		Post p;
294 		if( id.length > 0 ){
295 			p = m_ctrl.db.getPost(BsonObjectID.fromHexString(id));
296 			enforce(_postname == p.name, "URL does not match the edited post!");
297 		} else {
298 			p = new Post;
299 			p.category = "general";
300 			p.date = Clock.currTime().toUTC();
301 		}
302 		enforce(_auth.loginUser.mayPostInCategory(category), "You are now allowed to post in the '"~category~"' category.");
303 
304 		p.isPublic = isPublic;
305 		p.commentsAllowed = commentsAllowed;
306 		p.author = author;
307 		p.date = SysTime.fromSimpleString(date);
308 		p.category = category;
309 		p.slug = slug.length ? slug : header.length ? makeSlugFromHeader(header) : id;
310 		p.headerImage = headerImage;
311 		p.header = header;
312 		p.subHeader = subHeader;
313 		p.summary = summary;
314 		p.summaryTitle = summaryTitle;
315 		p.content = content;
316 		import std.array : split;
317 		p.filters = filters.split();
318 
319 		enforce(!m_ctrl.db.hasPost(p.slug) || m_ctrl.db.getPost(p.slug).id == p.id, "Post slug is already used for another article.");
320 
321 		if( id.length > 0 )
322 		{
323 			m_ctrl.db.modifyPost(p);
324 			_postname = p.name;
325 		}
326 		else
327 		{
328 			p.id = m_ctrl.db.addPost(p);
329 		}
330 		redirect(m_subPath~"posts/");
331 	}
332 
333 	@path("posts/:postname/files/:filename/delete") @errorDisplay!getEditPost
334 	void postDeleteFile(string _postname, string _filename, AuthInfo _auth)
335 	{
336 		m_ctrl.db.removeFile(_postname, _filename);
337 		redirect("../../");
338 	}
339 
340 	@path("posts/:postname/files/") @errorDisplay!getEditPost
341 	void postUploadFile(string _postname, HTTPServerRequest req, AuthInfo _auth)
342 	{
343 		import vibe.core.file;
344 		import vibe.stream.operations : readAll;
345 
346 import vibe.core.log;
347 logInfo("FILES %s %s", req.files.length, req.files.getAll("files"));
348 		foreach (f; req.files.byValue) {
349 logInfo("FILE %s", f.filename.name);
350 			auto fil = openFile(f.tempPath, FileMode.read);
351 			scope (exit) fil.close();
352 			m_ctrl.db.addFile(_postname, f.filename.name, fil.readAll());
353 		}
354 		redirect("../");
355 	}
356 
357 	private enum auth = before!performAuth("_auth");
358 
359 	private AuthInfo performAuth(HTTPServerRequest req, HTTPServerResponse res)
360 	{
361 		import vibe.inet.webform : formEncode;
362 
363 		string uname = req.session ? req.session.get("vibelog.loggedInUser", "") : "";
364 		User[string] users = m_ctrl.db.getAllUsers();
365 		auto pu = uname in users;
366 		if (pu is null) {
367 			redirect(m_subPath ~ "login?"~formEncode(["redirect": req.path]));
368 			return AuthInfo.init;
369 		}
370 		enforceHTTP(pu !is null, HTTPStatus.forbidden, "Not authorized to access this page.");
371 		return AuthInfo(*pu, users);
372 	}
373 
374 	mixin PrivateAccessProxy;
375 }
376 
377 struct AdminInfo
378 {
379 	import vibelog.info : VibeLogInfo;
380 	VibeLogInfo vli;
381 	alias vli this;
382 
383 	User loginUser;
384 	User[string] users;
385 	InetPath rootPath, managePath;
386 	string loginError;
387 
388 	import vibelog.settings : VibeLogSettings;
389 	this(AuthInfo auth, VibeLogSettings settings)
390 	{
391 		vli = VibeLogInfo(settings);
392 		loginUser = auth.loginUser;
393 		users = auth.users;
394 		this.settings = settings;
395 		rootPath = settings.siteURL.path;
396 		managePath = rootPath ~ settings.adminPrefix;
397 	}
398 }
399 
400 enum string mixAdminInfo = q{AdminInfo ai; alias ai this;};
401 enum string mixInitAdminInfo = q{ai = AdminInfo(auth, settings);};
402 
403 struct PostEditInfo
404 {
405 	mixin(mixAdminInfo);
406 
407 	import vibelog.config : Config;
408 	Config globalConfig;
409 
410 	import vibelog.post : Post;
411 	Post post;
412 
413 	string[] files;
414 	string error;
415 
416 	import vibelog.settings : VibeLogSettings;
417 	this(AuthInfo auth, VibeLogSettings settings)
418 	{
419 		mixin(mixInitAdminInfo);
420 	}
421 }
422 
423 struct ConfigEditInfo
424 {
425 	mixin(mixAdminInfo);
426 
427 	Config config;
428 	Config globalConfig;
429 
430 	import vibelog.settings : VibeLogSettings;
431 	this(AuthInfo auth, VibeLogSettings settings, Config config, Config globalConfig)
432 	{
433 		this(auth, settings);
434 		this.config = config;
435 		this.globalConfig = globalConfig;
436 	}
437 
438 	this(AuthInfo auth, VibeLogSettings settings)
439 	{
440 		mixin(mixInitAdminInfo);
441 	}
442 }
443 
444 struct ConfigsInfo
445 {
446 	mixin(mixAdminInfo);
447 
448 	import vibelog.config : Config;
449 	Config[] configs;
450 	string activeConfigName;
451 
452 	import vibelog.settings : VibeLogSettings;
453 	this(AuthInfo auth, VibeLogSettings settings, Config[] configs, string activeConfigName)
454 	{
455 		this(auth, settings);
456 		this.configs = configs;
457 		this.activeConfigName = activeConfigName;
458 	}
459 	this(AuthInfo auth, VibeLogSettings settings)
460 	{
461 		mixin(mixInitAdminInfo);
462 	}
463 }
464 
465 struct UserEditInfo
466 {
467 	mixin(mixAdminInfo);
468 
469 	import vibelog.config : Config;
470 	Config globalConfig;
471 
472 	import vibelog.user : User;
473 	User user;
474 
475 	import vibelog.settings : VibeLogSettings;
476 	this(AuthInfo auth, VibeLogSettings settings)
477 	{
478 		mixin(mixInitAdminInfo);
479 	}
480 }
481 
482 struct PostsInfo
483 {
484 	mixin(mixAdminInfo);
485 
486 	import vibelog.post : Post;
487 	Post[] posts;
488 
489 	import vibelog.settings : VibeLogSettings;
490 	this(AuthInfo auth, VibeLogSettings settings)
491 	{
492 		mixin(mixInitAdminInfo);
493 	}
494 }
495 
496 private struct AuthInfo {
497 	User loginUser;
498 	User[string] users;
499 }
500 
501 private void enforceAuth(bool cond, lazy string message = "Not authorized to perform this action!")
502 {
503 	if (!cond) throw new HTTPStatusException(HTTPStatus.forbidden, message);
504 }