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 }