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 }