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 
13 
14 void registerVibeLogWebAdmin(URLRouter router, VibeLogController controller)
15 {
16 	auto websettings = new WebInterfaceSettings;
17 	websettings.urlPrefix = (controller.settings.siteURL.path ~ controller.settings.adminPrefix).toString();
18 	router.registerWebInterface(new VibeLogWebAdmin(controller), websettings);
19 }
20 
21 private final class VibeLogWebAdmin {
22 	private {
23 		VibeLogController m_ctrl;
24 		VibeLogSettings m_settings;
25 		string m_subPath;
26 		string m_config;
27 	}
28 
29 	this(VibeLogController controller)
30 	{
31 		m_ctrl = controller;
32 		m_settings = controller.settings;
33 		m_subPath = (m_settings.siteURL.path ~ m_settings.adminPrefix).toString();
34 	}
35 
36 	// the whole admin interface needs authentication
37 	@auth:
38 
39 	void get(AuthInfo _auth)
40 	{
41 		auto ctx = makeContext(_auth);
42 		render!("vibelog.admin.home.dt", ctx);
43 	}
44 
45 	//
46 	// Configs
47 	//
48 
49 	@path("configs/")
50 	void getConfigs(AuthInfo _auth)
51 	{
52 		enforceAuth(_auth.loginUser.isConfigAdmin());
53 		auto ctx = makeContext(_auth);
54 		Config[] configs = m_ctrl.db.getAllConfigs();
55 		auto activeConfig = m_settings.configName;
56 		render!("vibelog.admin.editconfiglist.dt", ctx, configs, activeConfig);
57 	}
58 
59 	@path("configs/:configname/")
60 	void getConfigEdit(string _configname, AuthInfo _auth)
61 	{
62 		enforceAuth(_auth.loginUser.isConfigAdmin());
63 		auto ctx = makeContext(_auth);
64 		auto globalConfig = m_ctrl.db.getConfig("global", true);
65 		Config config = m_ctrl.db.getConfig(_configname);
66 		render!("vibelog.admin.editconfig.dt", ctx, globalConfig, config);
67 	}
68 
69 	@path("configs/:configname/")
70 	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)
71 	{
72 		import std.string;
73 
74 		enforceAuth(_auth.loginUser.isConfigAdmin());
75 		Config cfg = m_ctrl.db.getConfig(_configname);
76 		if( cfg.name == "global" )
77 			cfg.categories = categories.splitLines();
78 		else {
79 			cfg.categories = null;
80 			foreach( k, v; req.form ){
81 				if( k.startsWith("category_") )
82 					cfg.categories ~= k[9 .. $];
83 			}
84 		}
85 		cfg.language = language;
86 		cfg.copyrightString = copyrightString;
87 		cfg.feedTitle = feedTitle;
88 		cfg.feedLink = feedLink;
89 		cfg.feedDescription = feedDescription;
90 		cfg.feedImageTitle = feedImageTitle;
91 		cfg.feedImageUrl = feedImageUrl;
92 	
93 		m_ctrl.db.setConfig(cfg);
94 
95 		redirect(m_subPath ~ "configs/");
96 	}
97 
98 	@path("configs/:configname/delete")
99 	void postDeleteConfig(string _configname, AuthInfo _auth)
100 	{
101 		enforceAuth(_auth.loginUser.isConfigAdmin());
102 		m_ctrl.db.deleteConfig(_configname);
103 		redirect(m_subPath ~ "configs/");
104 	}
105 
106 
107 	//
108 	// Users
109 	//
110 
111 	@path("users/")
112 	void getUsers(AuthInfo _auth)
113 	{
114 		auto ctx = makeContext(_auth);
115 		render!("vibelog.admin.edituserlist.dt", ctx);
116 	}
117 
118 	@path("users/:username/")
119 	void getUserEdit(string _username, AuthInfo _auth)
120 	{
121 		auto ctx = makeContext(_auth);
122 		auto globalConfig = m_ctrl.db.getConfig("global", true);
123 		User user = m_ctrl.db.getUser(_username);
124 		render!("vibelog.admin.edituser.dt", ctx, globalConfig, user);
125 	}
126 
127 	@path("users/:username/")
128 	void postPutUser(string id, string username, string password, string name, string email, string passwordConfirmation, Nullable!string oldPassword, string _username, HTTPServerRequest req, AuthInfo _auth)
129 	{
130 		import vibe.crypto.passwordhash;
131 		import vibe.data.bson : BsonObjectID;
132 
133 		User usr;
134 		if( id.length > 0 ){
135 			enforce(_auth.loginUser.isUserAdmin() || username == _auth.loginUser.username,
136 				"You can only change your own account.");
137 			usr = m_ctrl.db.getUser(BsonObjectID.fromHexString(id));
138 			enforce(usr.username == username, "Cannot change the user name!");
139 		} else {
140 			enforce(_auth.loginUser.isUserAdmin(), "You are not allowed to add users.");
141 			usr = new User;
142 			usr.username = username;
143 			foreach (u; _auth.users)
144 				enforce(u.username != usr.username, "A user with the specified user name already exists!");
145 		}
146 		enforce(password == passwordConfirmation, "Passwords do not match!");
147 
148 		usr.name = name;
149 		usr.email = email;
150 
151 		if (password.length) {
152 			enforce(_auth.loginUser.isUserAdmin() || testSimplePasswordHash(oldPassword, usr.password), "Old password does not match.");
153 			usr.password = generateSimplePasswordHash(password);
154 		}
155 
156 		if (_auth.loginUser.isUserAdmin()) {
157 			usr.groups = null;
158 			foreach( k, v; req.form ){
159 				if( k.startsWith("group_") )
160 					usr.groups ~= k[6 .. $];
161 			}
162 
163 			usr.allowedCategories = null;
164 			foreach( k, v; req.form ){
165 				if( k.startsWith("category_") )
166 					usr.allowedCategories ~= k[9 .. $];
167 			}
168 		}
169 
170 		if( id.length > 0 ){
171 			m_ctrl.db.modifyUser(usr);
172 		} else {
173 			usr._id = m_ctrl.db.addUser(usr);
174 		}
175 
176 		if (_auth.loginUser.isUserAdmin()) redirect(m_subPath~"users/");
177 		else redirect(m_subPath);
178 	}
179 
180 	@path("users/:username/delete")
181 	void postDeleteUser(string _username, AuthInfo _auth)
182 	{
183 		enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to delete users!");
184 		enforce(_auth.loginUser.username != _username, "Cannot delete the own user account!");
185 		foreach (usr; _auth.users)
186 			if (usr.username == _username) {
187 				m_ctrl.db.deleteUser(usr._id);
188 				redirect(m_subPath ~ "users/");
189 				return;
190 			}
191 		
192 		// fall-through (404)
193 	}
194 
195 	@path("users/")
196 	void postAddUser(string username, AuthInfo _auth)
197 	{
198 		enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to add users!");
199 		if (username !in _auth.users) {
200 			auto u = new User;
201 			u.username = username;
202 			m_ctrl.db.addUser(u);
203 		}
204 		redirect(m_subPath ~ "users/" ~ username ~ "/");
205 	}
206 
207 	//
208 	// Posts
209 	//
210 
211 	@path("posts/")
212 	void getPosts(AuthInfo _auth)
213 	{
214 		auto ctx = makeContext(_auth);
215 		Post[] posts;
216 		m_ctrl.db.getAllPosts(0, (size_t idx, Post post){
217 			if (_auth.loginUser.isPostAdmin() || post.author == _auth.loginUser.username
218 				|| _auth.loginUser.mayPostInCategory(post.category))
219 			{
220 				posts ~= post;
221 			}
222 			return true;
223 		});
224 		render!("vibelog.admin.editpostslist.dt", ctx, posts);
225 	}
226 
227 	void getMakePost(AuthInfo _auth, string _error = null)
228 	{
229 		auto ctx = makeContext(_auth);
230 		auto globalConfig = m_ctrl.db.getConfig("global", true);
231 		Post post;
232 		Comment[] comments;
233 		string[] files;
234 		string error = _error;
235 		render!("vibelog.admin.editpost.dt", ctx, globalConfig, post, comments, files, error);
236 	}
237 
238 	@auth @errorDisplay!getMakePost
239 	void postMakePost(bool isPublic, bool commentsAllowed, string author,
240 		string date, string category, string slug, string headerImage, string header, string subHeader,
241 		string content, AuthInfo _auth)
242 	{
243 		postPutPost(null, isPublic, commentsAllowed, author, date, category, slug, headerImage, header, subHeader, content, null, _auth);
244 	}
245 
246 	@path("posts/:postname/")
247 	void getEditPost(string _postname, AuthInfo _auth, string _error = null)
248 	{
249 		auto ctx = makeContext(_auth);
250 		auto globalConfig = m_ctrl.db.getConfig("global", true);
251 		auto post = m_ctrl.db.getPost(_postname);
252 		auto comments = m_ctrl.db.getComments(post.id, true);
253 		auto files = m_ctrl.db.getFiles(_postname);
254 		auto error = _error;
255 		render!("vibelog.admin.editpost.dt", ctx, globalConfig, post, comments, files, error);
256 	}
257 
258 	@path("posts/:postname/delete")
259 	void postDeletePost(string id, string _postname, AuthInfo _auth)
260 	{
261 		import vibe.data.bson : BsonObjectID;
262 		// FIXME: check permissons!
263 		auto bid = BsonObjectID.fromHexString(id);
264 		m_ctrl.db.deletePost(bid);
265 		redirect(m_subPath ~ "posts/");
266 	}
267 
268 	@path("posts/:postname/set_comment_public") @errorDisplay!getEditPost
269 	void postSetCommentPublic(string id, string _postname, bool public_, AuthInfo _auth)
270 	{
271 		import vibe.data.bson : BsonObjectID;
272 		// FIXME: check permissons!
273 		auto bid = BsonObjectID.fromHexString(id);
274 		m_ctrl.db.setCommentPublic(bid, public_);
275 		redirect(m_subPath ~ "posts/"~_postname~"/");
276 	}
277 
278 	@path("posts/:postname/") @errorDisplay!getEditPost
279 	void postPutPost(string id, bool isPublic, bool commentsAllowed, string author,
280 		string date, string category, string slug, string headerImage, string header, string subHeader,
281 		string content, string _postname, AuthInfo _auth)
282 	{
283 		import vibe.data.bson : BsonObjectID;
284 
285 		Post p;
286 		if( id.length > 0 ){
287 			p = m_ctrl.db.getPost(BsonObjectID.fromHexString(id));
288 			enforce(_postname == p.name, "URL does not match the edited post!");
289 		} else {
290 			p = new Post;
291 			p.category = "default";
292 			p.date = Clock.currTime().toUTC();
293 		}
294 		enforce(_auth.loginUser.mayPostInCategory(category), "You are now allowed to post in the '"~category~"' category.");
295 
296 		p.isPublic = isPublic;
297 		p.commentsAllowed = commentsAllowed;
298 		p.author = author;
299 		p.date = SysTime.fromSimpleString(date);
300 		p.category = category;
301 		p.slug = slug.length ? slug : makeSlugFromHeader(header);
302 		p.headerImage = headerImage;
303 		p.header = header;
304 		p.subHeader = subHeader;
305 		p.content = content;
306 
307 		enforce(!m_ctrl.db.hasPost(p.slug) || m_ctrl.db.getPost(p.slug).id == p.id, "Post slug is already used for another article.");
308 
309 		if( id.length > 0 ){
310 			m_ctrl.db.modifyPost(p);
311 			_postname = p.name;
312 		} else {
313 			p.id = m_ctrl.db.addPost(p);
314 		}
315 		redirect(m_subPath~"posts/");
316 	}
317 
318 	@path("posts/:postname/files/:filename/delete") @errorDisplay!getEditPost
319 	void postDeleteFile(string _postname, string _filename, AuthInfo _auth)
320 	{
321 		m_ctrl.db.removeFile(_postname, _filename);
322 		redirect("../../");
323 	}
324 
325 	@path("posts/:postname/files/") @errorDisplay!getEditPost
326 	void postUploadFile(string _postname, HTTPServerRequest req, AuthInfo _auth)
327 	{
328 		import vibe.core.file;
329 
330 import vibe.core.log;
331 logInfo("FILES %s %s", req.files.length, req.files.getAll("files"));
332 		foreach (f; req.files) {
333 logInfo("FILE %s", f.filename);
334 			auto fil = openFile(f.tempPath, FileMode.read);
335 			scope (exit) fil.close();
336 			m_ctrl.db.addFile(_postname, f.filename.toString(), fil);
337 		}
338 		redirect("../");
339 	}
340 
341 	private auto makeContext(AuthInfo auth)
342 	{
343 		static struct S {
344 			User loginUser;
345 			User[string] users;
346 			VibeLogSettings settings;
347 			Path rootPath;
348 		}
349 
350 		S s;
351 		s.loginUser = auth.loginUser;
352 		s.users = auth.users;
353 		s.settings = m_settings;
354 		s.rootPath = m_settings.siteURL.path ~ m_settings.adminPrefix;
355 		return s;
356 	}
357 
358 	private enum auth = before!performAuth("_auth");
359 
360 	private AuthInfo performAuth(HTTPServerRequest req, HTTPServerResponse res)
361 	{
362 		import vibe.crypto.passwordhash;
363 		import vibe.http.auth.basic_auth;
364 
365 		User[string] users = m_ctrl.db.getAllUsers();
366 		bool testauth(string user, string password)
367 		{
368 			auto pu = user in users;
369 			if( pu is null ) return false;
370 			return testSimplePasswordHash(pu.password, password);
371 		}
372 		string username = performBasicAuth(req, res, "VibeLog admin area", &testauth);
373 		auto pusr = username in users;
374 		assert(pusr, "Authorized with unknown username !?");
375 		return AuthInfo(*pusr, users);
376 	}
377 
378 	mixin PrivateAccessProxy;
379 }
380 
381 private struct AuthInfo {
382 	User loginUser;
383 	User[string] users;
384 }
385 
386 private void enforceAuth(bool cond, lazy string message = "Not authorized to perform this action!")
387 {
388 	if (!cond) throw new HTTPStatusException(HTTPStatus.forbidden, message);
389 }