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 }