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() || validatePasswordHash(oldPassword.get, usr.password), "Old password does not match.");
170 			usr.password = generatePasswordHash(password);
171 		}
172 
173 		if (_auth.loginUser.isUserAdmin()) {
174 			usr.groups = null;
175 			foreach (k, v; req.form.byKeyValue) {
176 				if (k.startsWith("group_"))
177 					usr.groups ~= k[6 .. $];
178 			}
179 
180 			usr.allowedCategories = null;
181 			foreach (k, v; req.form.byKeyValue) {
182 				if (k.startsWith("category_"))
183 					usr.allowedCategories ~= k[9 .. $];
184 			}
185 		}
186 
187 		if( id.length > 0 ){
188 			m_ctrl.db.modifyUser(usr);
189 		} else {
190 			usr._id = m_ctrl.db.addUser(usr);
191 		}
192 
193 		if (_auth.loginUser.isUserAdmin()) redirect(m_subPath~"users/");
194 		else redirect(m_subPath);
195 	}
196 
197 	@path("users/:username/delete")
198 	void postDeleteUser(string _username, AuthInfo _auth)
199 	{
200 		enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to delete users!");
201 		enforce(_auth.loginUser.username != _username, "Cannot delete the own user account!");
202 		foreach (usr; _auth.users)
203 			if (usr.username == _username) {
204 				m_ctrl.db.deleteUser(usr._id);
205 				redirect(m_subPath ~ "users/");
206 				return;
207 			}
208 		
209 		// fall-through (404)
210 	}
211 
212 	@path("users/")
213 	void postAddUser(string username, AuthInfo _auth)
214 	{
215 		enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to add users!");
216 		if (username !in _auth.users) {
217 			auto u = new User;
218 			u.username = username;
219 			m_ctrl.db.addUser(u);
220 		}
221 		redirect(m_subPath ~ "users/" ~ username ~ "/");
222 	}
223 
224 	//
225 	// Posts
226 	//
227 
228 	@path("posts/")
229 	void getPosts(AuthInfo _auth)
230 	{
231 		auto info = PostsInfo(_auth, m_settings);
232 		m_ctrl.db.getAllPosts(0, (size_t idx, Post post){
233 			if (_auth.loginUser.isPostAdmin() || post.author == _auth.loginUser.username
234 				|| _auth.loginUser.mayPostInCategory(post.category))
235 			{
236 				info.posts ~= post;
237 			}
238 			return true;
239 		});
240 
241 		render!("vibelog.admin.editpostslist.dt", info);
242 	}
243 
244 	@path("make_post")
245 	void getMakePost(AuthInfo _auth, string _error = null)
246 	{
247 		auto info = PostEditInfo(_auth, m_settings);
248 		info.globalConfig = m_ctrl.db.getConfig("global", true);
249 		info.error = _error;
250 
251 		render!("vibelog.admin.editpost.dt", info);
252 	}
253 
254 	@auth @errorDisplay!getMakePost
255 	void postMakePost(bool isPublic, bool commentsAllowed, string author,
256 		string date, string category, string slug, string headerImage, string header, string subHeader,
257 		string content, string filters, AuthInfo _auth)
258 	{
259 		postPutPost(null, isPublic, commentsAllowed, author, date, category, slug, headerImage, header, subHeader, content, filters, null, _auth);
260 	}
261 
262 	@path("posts/:postname/")
263 	void getEditPost(string _postname, AuthInfo _auth, string _error = null)
264 	{
265 		auto info = PostEditInfo(_auth, m_settings);
266 		info.globalConfig = m_ctrl.db.getConfig("global", true);
267 		info.post = m_ctrl.db.getPost(_postname);
268 		info.files = m_ctrl.db.getFiles(_postname);
269 		info.error = _error;
270 		render!("vibelog.admin.editpost.dt", info);
271 	}
272 
273 	@path("posts/:postname/delete")
274 	void postDeletePost(string id, string _postname, AuthInfo _auth)
275 	{
276 		import vibe.data.bson : BsonObjectID;
277 		// FIXME: check permissons!
278 		auto bid = BsonObjectID.fromHexString(id);
279 		m_ctrl.db.deletePost(bid);
280 		redirect(m_subPath ~ "posts/");
281 	}
282 
283 	@path("posts/:postname/") @errorDisplay!getEditPost
284 	void postPutPost(string id, bool isPublic, bool commentsAllowed, string author,
285 		string date, string category, string slug, string headerImage, string header, string subHeader,
286 		string content, string filters, string _postname, AuthInfo _auth)
287 	{
288 		import vibe.data.bson : BsonObjectID;
289 
290 		Post p;
291 		if( id.length > 0 ){
292 			p = m_ctrl.db.getPost(BsonObjectID.fromHexString(id));
293 			enforce(_postname == p.name, "URL does not match the edited post!");
294 		} else {
295 			p = new Post;
296 			p.category = "general";
297 			p.date = Clock.currTime().toUTC();
298 		}
299 		enforce(_auth.loginUser.mayPostInCategory(category), "You are now allowed to post in the '"~category~"' category.");
300 
301 		p.isPublic = isPublic;
302 		p.commentsAllowed = commentsAllowed;
303 		p.author = author;
304 		p.date = SysTime.fromSimpleString(date);
305 		p.category = category;
306 		p.slug = slug.length ? slug : header.length ? makeSlugFromHeader(header) : id;
307 		p.headerImage = headerImage;
308 		p.header = header;
309 		p.subHeader = subHeader;
310 		p.content = content;
311 		import std.array : split;
312 		p.filters = filters.split();
313 
314 		enforce(!m_ctrl.db.hasPost(p.slug) || m_ctrl.db.getPost(p.slug).id == p.id, "Post slug is already used for another article.");
315 
316 		if( id.length > 0 )
317 		{
318 			m_ctrl.db.modifyPost(p);
319 			_postname = p.name;
320 		}
321 		else
322 		{
323 			p.id = m_ctrl.db.addPost(p);
324 		}
325 		redirect(m_subPath~"posts/");
326 	}
327 
328 	@path("posts/:postname/files/:filename/delete") @errorDisplay!getEditPost
329 	void postDeleteFile(string _postname, string _filename, AuthInfo _auth)
330 	{
331 		m_ctrl.db.removeFile(_postname, _filename);
332 		redirect("../../");
333 	}
334 
335 	@path("posts/:postname/files/") @errorDisplay!getEditPost
336 	void postUploadFile(string _postname, HTTPServerRequest req, AuthInfo _auth)
337 	{
338 		import vibe.core.file;
339 		import vibe.stream.operations : readAll;
340 
341 import vibe.core.log;
342 logInfo("FILES %s %s", req.files.length, req.files.getAll("files"));
343 		foreach (f; req.files.byValue) {
344 logInfo("FILE %s", f.filename.name);
345 			auto fil = openFile(f.tempPath, FileMode.read);
346 			scope (exit) fil.close();
347 			m_ctrl.db.addFile(_postname, f.filename.name, fil.readAll());
348 		}
349 		redirect("../");
350 	}
351 
352 	private enum auth = before!performAuth("_auth");
353 
354 	private AuthInfo performAuth(HTTPServerRequest req, HTTPServerResponse res)
355 	{
356 		import vibe.inet.webform : formEncode;
357 
358 		string uname = req.session ? req.session.get("vibelog.loggedInUser", "") : "";
359 		User[string] users = m_ctrl.db.getAllUsers();
360 		auto pu = uname in users;
361 		if (pu is null) {
362 			redirect(m_subPath ~ "login?"~formEncode(["redirect": req.path]));
363 			return AuthInfo.init;
364 		}
365 		enforceHTTP(pu !is null, HTTPStatus.forbidden, "Not authorized to access this page.");
366 		return AuthInfo(*pu, users);
367 	}
368 
369 	mixin PrivateAccessProxy;
370 }
371 
372 struct AdminInfo
373 {
374 	import vibelog.info : VibeLogInfo;
375 	VibeLogInfo vli;
376 	alias vli this;
377 
378 	User loginUser;
379 	User[string] users;
380 	InetPath rootPath, managePath;
381 	string loginError;
382 
383 	import vibelog.settings : VibeLogSettings;
384 	this(AuthInfo auth, VibeLogSettings settings)
385 	{
386 		vli = VibeLogInfo(settings);
387 		loginUser = auth.loginUser;
388 		users = auth.users;
389 		this.settings = settings;
390 		rootPath = settings.siteURL.path;
391 		managePath = rootPath ~ settings.adminPrefix;
392 	}
393 }
394 
395 enum string mixAdminInfo = q{AdminInfo ai; alias ai this;};
396 enum string mixInitAdminInfo = q{ai = AdminInfo(auth, settings);};
397 
398 struct PostEditInfo
399 {
400 	mixin(mixAdminInfo);
401 
402 	import vibelog.config : Config;
403 	Config globalConfig;
404 
405 	import vibelog.post : Post;
406 	Post post;
407 
408 	string[] files;
409 	string error;
410 
411 	import vibelog.settings : VibeLogSettings;
412 	this(AuthInfo auth, VibeLogSettings settings)
413 	{
414 		mixin(mixInitAdminInfo);
415 	}
416 }
417 
418 struct ConfigEditInfo
419 {
420 	mixin(mixAdminInfo);
421 
422 	Config config;
423 	Config globalConfig;
424 
425 	import vibelog.settings : VibeLogSettings;
426 	this(AuthInfo auth, VibeLogSettings settings, Config config, Config globalConfig)
427 	{
428 		this(auth, settings);
429 		this.config = config;
430 		this.globalConfig = globalConfig;
431 	}
432 
433 	this(AuthInfo auth, VibeLogSettings settings)
434 	{
435 		mixin(mixInitAdminInfo);
436 	}
437 }
438 
439 struct ConfigsInfo
440 {
441 	mixin(mixAdminInfo);
442 
443 	import vibelog.config : Config;
444 	Config[] configs;
445 	string activeConfigName;
446 
447 	import vibelog.settings : VibeLogSettings;
448 	this(AuthInfo auth, VibeLogSettings settings, Config[] configs, string activeConfigName)
449 	{
450 		this(auth, settings);
451 		this.configs = configs;
452 		this.activeConfigName = activeConfigName;
453 	}
454 	this(AuthInfo auth, VibeLogSettings settings)
455 	{
456 		mixin(mixInitAdminInfo);
457 	}
458 }
459 
460 struct UserEditInfo
461 {
462 	mixin(mixAdminInfo);
463 
464 	import vibelog.config : Config;
465 	Config globalConfig;
466 
467 	import vibelog.user : User;
468 	User user;
469 
470 	import vibelog.settings : VibeLogSettings;
471 	this(AuthInfo auth, VibeLogSettings settings)
472 	{
473 		mixin(mixInitAdminInfo);
474 	}
475 }
476 
477 struct PostsInfo
478 {
479 	mixin(mixAdminInfo);
480 
481 	import vibelog.post : Post;
482 	Post[] posts;
483 
484 	import vibelog.settings : VibeLogSettings;
485 	this(AuthInfo auth, VibeLogSettings settings)
486 	{
487 		mixin(mixInitAdminInfo);
488 	}
489 }
490 
491 private struct AuthInfo {
492 	User loginUser;
493 	User[string] users;
494 }
495 
496 private void enforceAuth(bool cond, lazy string message = "Not authorized to perform this action!")
497 {
498 	if (!cond) throw new HTTPStatusException(HTTPStatus.forbidden, message);
499 }