1 /** 2 (module summary) 3 4 Copyright: © 2012-2016 RejectedSoftware e.K. 5 License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module vibenews.admin; 9 10 import vibenews.controller; 11 import vibenews.vibenews; 12 13 import userman.db.controller : User; 14 static import userman.db.controller; 15 16 import vibe.core.log; 17 import vibe.data.bson; 18 import vibe.http.router; 19 import vibe.http.server; 20 import vibe.http.fileserver; 21 import vibe.textfilter.urlencode; 22 import vibe.utils.validation; 23 24 import std.algorithm : map; 25 import std.array; 26 import std.conv; 27 import std.exception; 28 import std.string; 29 import std.variant; 30 31 class AdminInterface { 32 private { 33 Controller m_ctrl; 34 } 35 36 this(Controller ctrl) 37 { 38 m_ctrl = ctrl; 39 } 40 41 void listen() 42 { 43 auto vnsettings = m_ctrl.settings; 44 45 auto settings = new HTTPServerSettings; 46 settings.port = vnsettings.adminPort; 47 settings.bindAddresses = vnsettings.adminBindAddresses; 48 49 auto router = new URLRouter; 50 register(router); 51 52 listenHTTP(settings, router); 53 } 54 55 void register(URLRouter router) 56 { 57 router.get("/", &showAdminPanel); 58 router.post("/categories/create", &createGroupCategory); 59 router.get("/categories/:category/show", &showGroupCategory); 60 router.post("/categories/:category/update", &updateGroupCategory); 61 router.post("/categories/:category/delete", &deleteGroupCategory); 62 router.post("/reclassify_spam", &reclassifySpam); 63 router.post("/groups/create", &createGroup); 64 router.post("/groups/repair-numbers", &repairGroupNumbers); 65 router.post("/groups/repair-threads", &repairGroupThreads); 66 router.get("/groups/:groupname/show", &showGroup); 67 router.post("/groups/:groupname/update", &updateGroup); 68 router.post("/groups/:groupname/purge", &purgeGroup); 69 router.get("/groups/:groupname/articles", &showArticles); 70 router.post("/articles/:articleid/activate", &activateArticle); 71 router.post("/articles/:articleid/deactivate", &deactivateArticle); 72 router.post("/articles/:articleid/mark_ham", &markAsHam); 73 router.post("/articles/:articleid/mark_spam", &markAsSpam); 74 router.get("/users/", &showListUsers); 75 router.get("/users/:user/", &showUser); 76 router.post("/users/:user/update", &updateUser); 77 router.post("/users/:user/delete", &deleteUser); 78 router.post("/users/deleteOrphaned", &postDeleteOrphanedUsers); 79 router.get("*", serveStaticFiles("public")); 80 } 81 82 void showAdminPanel(HTTPServerRequest req, HTTPServerResponse res) 83 { 84 struct Info { 85 VibeNewsSettings settings; 86 } 87 auto info = Info(m_ctrl.settings); 88 89 Group[] groups; 90 GroupCategory[] categories; 91 m_ctrl.enumerateGroups((idx, group){ groups ~= group; }, true); 92 m_ctrl.enumerateGroupCategories((idx, cat){ categories ~= cat; }); 93 res.render!("vibenews.admin.index.dt", req, info, groups, categories); 94 } 95 96 void showGroupCategory(HTTPServerRequest req, HTTPServerResponse res) 97 { 98 struct Info { 99 VibeNewsSettings settings; 100 } 101 auto info = Info(m_ctrl.settings); 102 103 auto category = m_ctrl.getGroupCategory(BsonObjectID.fromString(req.params["category"])); 104 Group[] groups; 105 m_ctrl.enumerateGroups((idx, grp){ groups ~= grp; }); 106 res.render!("vibenews.admin.editcategory.dt", req, info, category, groups); 107 } 108 109 void updateGroupCategory(HTTPServerRequest req, HTTPServerResponse res) 110 { 111 auto id = BsonObjectID.fromString(req.params["category"]); 112 auto caption = req.form["caption"]; 113 auto index = req.form["index"].to!int(); 114 BsonObjectID[] groups; 115 m_ctrl.enumerateGroups((idx, grp){ if( grp._id.toString() in req.form ) groups ~= grp._id; }); 116 m_ctrl.updateGroupCategory(id, caption, index, groups); 117 res.redirect("/categories/"~id.toString()~"/show"); 118 } 119 120 void deleteGroupCategory(HTTPServerRequest req, HTTPServerResponse res) 121 { 122 auto id = BsonObjectID.fromString(req.params["category"]); 123 m_ctrl.deleteGroupCategory(id); 124 res.redirect("/"); 125 } 126 127 void showGroup(HTTPServerRequest req, HTTPServerResponse res) 128 { 129 struct Info { 130 VibeNewsSettings settings; 131 } 132 auto info = Info(m_ctrl.settings); 133 134 auto group = m_ctrl.getGroupByName(req.params["groupname"], true); 135 res.render!("vibenews.admin.editgroup.dt", req, info, group); 136 } 137 138 void createGroupCategory(HTTPServerRequest req, HTTPServerResponse res) 139 { 140 auto id = m_ctrl.createGroupCategory(req.form["caption"], req.form["index"].to!int()); 141 res.redirect("/categories/"~id.toString()~"/show"); 142 } 143 144 void reclassifySpam(HTTPServerRequest req, HTTPServerResponse res) 145 { 146 m_ctrl.reclassifySpam(); 147 res.redirect("/"); 148 } 149 150 void createGroup(HTTPServerRequest req, HTTPServerResponse res) 151 { 152 enforce(!m_ctrl.groupExists(req.form["name"], true), "A group with the specified name already exists"); 153 154 Group group; 155 group._id = BsonObjectID.generate(); 156 group.active = false; 157 group.name = req.form["name"]; 158 group.caption = req.form["caption"]; 159 m_ctrl.addGroup(group); 160 161 res.redirect("/groups/"~urlEncode(group.name)~"/show"); 162 } 163 164 void updateGroup(HTTPServerRequest req, HTTPServerResponse res) 165 { 166 auto group = m_ctrl.getGroupByName(req.params["groupname"], true); 167 group.caption = req.form["caption"]; 168 group.description = req.form["description"]; 169 group.active = ("active" in req.form) !is null; 170 group.readOnlyAuthTags = req.form["roauthtags"].split(",").map!(s => authGroupPrefix ~ strip(s))().array(); 171 group.readWriteAuthTags = req.form["rwauthtags"].split(",").map!(s => authGroupPrefix ~ strip(s))().array(); 172 m_ctrl.updateGroup(group); 173 res.redirect("/groups/"~urlEncode(group.name)~"/show"); 174 } 175 176 void purgeGroup(HTTPServerRequest req, HTTPServerResponse res) 177 { 178 m_ctrl.purgeGroup(req.params["groupname"]); 179 res.redirect("/groups/"~req.params["groupname"]~"/show"); 180 } 181 182 void repairGroupNumbers(HTTPServerRequest req, HTTPServerResponse res) 183 { 184 m_ctrl.repairGroupNumbers(); 185 res.redirect("/"); 186 } 187 188 void repairGroupThreads(HTTPServerRequest req, HTTPServerResponse res) 189 { 190 m_ctrl.repairThreads(); 191 res.redirect("/"); 192 } 193 194 void showArticles(HTTPServerRequest req, HTTPServerResponse res) 195 { 196 struct Info { 197 VibeNewsSettings settings; 198 enum articlesPerPage = 20; 199 string groupname; 200 int page; 201 Article[] articles; 202 int articleCount; 203 int pageCount; 204 bool onlyActive; 205 } 206 207 Info info; 208 info.settings = m_ctrl.settings; 209 info.groupname = req.params["groupname"]; 210 info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0; 211 info.onlyActive = req.query.get("only_active", "") == "1"; 212 if (info.onlyActive) { 213 m_ctrl.enumerateActiveArticlesBackwards(info.groupname, info.page*info.articlesPerPage, info.articlesPerPage, (ref art){ info.articles ~= art; }); 214 info.articleCount = cast(int)m_ctrl.getActiveArticlesCount(info.groupname); 215 } else { 216 m_ctrl.enumerateAllArticlesBackwards(info.groupname, info.page*info.articlesPerPage, info.articlesPerPage, (ref art){ info.articles ~= art; }); 217 info.articleCount = cast(int)m_ctrl.getAllArticlesCount(info.groupname); 218 } 219 info.pageCount = (info.articleCount-1)/info.articlesPerPage + 1; 220 221 res.render!("vibenews.admin.listarticles.dt", req, info); 222 } 223 224 void activateArticle(HTTPServerRequest req, HTTPServerResponse res) 225 { 226 auto artid = BsonObjectID.fromString(req.params["articleid"]); 227 m_ctrl.activateArticle(artid); 228 redirectBackToArticles(req, res); 229 } 230 231 void deactivateArticle(HTTPServerRequest req, HTTPServerResponse res) 232 { 233 auto artid = BsonObjectID.fromString(req.params["articleid"]); 234 m_ctrl.deactivateArticle(artid); 235 redirectBackToArticles(req, res); 236 } 237 238 void markAsSpam(HTTPServerRequest req, HTTPServerResponse res) 239 { 240 auto artid = BsonObjectID.fromString(req.params["articleid"]); 241 m_ctrl.markAsSpam(artid, true); 242 redirectBackToArticles(req, res); 243 } 244 245 void markAsHam(HTTPServerRequest req, HTTPServerResponse res) 246 { 247 auto artid = BsonObjectID.fromString(req.params["articleid"]); 248 m_ctrl.markAsSpam(artid, false); 249 redirectBackToArticles(req, res); 250 } 251 252 private void redirectBackToArticles(HTTPServerRequest req, HTTPServerResponse res) 253 { 254 string suff = req.form.get("only_active", "") == "1" ? "&only_active=1" : null; 255 res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]~suff); 256 } 257 258 void showListUsers(HTTPServerRequest req, HTTPServerResponse res) 259 { 260 struct Info { 261 VibeNewsSettings settings; 262 enum itemsPerPage = 20; 263 UserInfo[] users; 264 int page; 265 int itemCount; 266 int pageCount; 267 } 268 269 Info info; 270 info.settings = m_ctrl.settings; 271 info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0; 272 m_ctrl.enumerateUsers(info.page*info.itemsPerPage, info.itemsPerPage, (ref user){ 273 info.users ~= getUserInfo(m_ctrl, user); 274 }); 275 info.itemCount = cast(int)m_ctrl.getUserCount(); 276 info.pageCount = (info.itemCount-1)/info.itemsPerPage + 1; 277 278 res.render!("vibenews.admin.listusers.dt", req, info); 279 } 280 281 void showUser(HTTPServerRequest req, HTTPServerResponse res) 282 { 283 struct Info { 284 VibeNewsSettings settings; 285 UserInfo user; 286 } 287 auto uid = User.ID.fromString(req.params["user"]); 288 User usr = m_ctrl.getUser(uid); 289 Info info; 290 info.settings = m_ctrl.settings; 291 info.user = getUserInfo(m_ctrl, usr); 292 res.render!("vibenews.admin.edituser.dt", req, info); 293 } 294 295 void updateUser(HTTPServerRequest req, HTTPServerResponse res) 296 { 297 import std.algorithm.iteration : splitter; 298 299 auto uid = User.ID.fromString(req.params["user"]); 300 auto user = m_ctrl.getUser(uid); 301 if (auto pv = "email" in req.form) { 302 validateEmail(*pv); 303 user.email = user.name = *pv; 304 } 305 if (auto pv = "fullName" in req.form) user.fullName = *pv; 306 if (auto pv = "groups" in req.form) { 307 user.groups.length = 0; 308 foreach (grp; (*pv).splitter(",").map!(g => authGroupPrefix ~ g.strip())) { 309 try user.groups ~= m_ctrl.getAuthGroupByName(grp).id; 310 catch (Exception) { 311 m_ctrl.userManController.addGroup(grp, "VibeNews authentication group"); 312 user.groups ~= m_ctrl.getAuthGroupByName(grp).id; 313 } 314 } 315 } 316 user.active = ("active" in req.form) !is null; 317 user.banned = ("banned" in req.form) !is null; 318 m_ctrl.updateUser(user); 319 320 res.redirect("/users/"); 321 } 322 323 void deleteUser(HTTPServerRequest req, HTTPServerResponse res) 324 { 325 m_ctrl.deleteUser(User.ID.fromString(req.params["user"])); 326 res.redirect("/users/"); 327 } 328 329 void postDeleteOrphanedUsers(HTTPServerRequest req, HTTPServerResponse res) 330 { 331 m_ctrl.deleteOrphanedUsers(); 332 res.redirect("/users/"); 333 } 334 335 } 336 337 struct UserInfo { 338 User user; 339 alias user this; 340 ulong messageCount; 341 ulong deletedMessageCount; 342 string[] groupStrings; 343 } 344 345 private UserInfo getUserInfo(Controller ctrl, User user) 346 { 347 UserInfo nfo; 348 nfo.user = user; 349 ctrl.getUserMessageCount(user.email, nfo.messageCount, nfo.deletedMessageCount); 350 foreach (grpname; user.groups) { 351 if (!grpname.startsWith(authGroupPrefix)) continue; 352 grpname = grpname[authGroupPrefix.length .. $]; 353 nfo.groupStrings ~= grpname; 354 } 355 return nfo; 356 }