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.crypto.passwordhash; 18 import vibe.data.bson; 19 import vibe.http.router; 20 import vibe.http.server; 21 import vibe.http.fileserver; 22 import vibe.textfilter.urlencode; 23 import vibe.utils.validation; 24 25 import std.algorithm : map; 26 import std.array; 27 import std.conv; 28 import std.exception; 29 import std.string; 30 import std.variant; 31 32 class AdminInterface { 33 private { 34 Controller m_ctrl; 35 } 36 37 this(Controller ctrl) 38 { 39 m_ctrl = ctrl; 40 } 41 42 void listen() 43 { 44 auto vnsettings = m_ctrl.settings; 45 46 auto settings = new HTTPServerSettings; 47 settings.port = vnsettings.adminPort; 48 settings.bindAddresses = vnsettings.adminBindAddresses; 49 50 auto router = new URLRouter; 51 register(router); 52 53 listenHTTP(settings, router); 54 } 55 56 void register(URLRouter router) 57 { 58 router.get("/", &showAdminPanel); 59 router.post("/categories/create", &createGroupCategory); 60 router.get("/categories/:category/show", &showGroupCategory); 61 router.post("/categories/:category/update", &updateGroupCategory); 62 router.post("/categories/:category/delete", &deleteGroupCategory); 63 router.post("/reclassify_spam", &reclassifySpam); 64 router.post("/groups/create", &createGroup); 65 router.post("/groups/repair-numbers", &repairGroupNumbers); 66 router.post("/groups/repair-threads", &repairGroupThreads); 67 router.get("/groups/:groupname/show", &showGroup); 68 router.post("/groups/:groupname/update", &updateGroup); 69 router.post("/groups/:groupname/purge", &purgeGroup); 70 router.get("/groups/:groupname/articles", &showArticles); 71 router.post("/articles/:articleid/activate", &activateArticle); 72 router.post("/articles/:articleid/deactivate", &deactivateArticle); 73 router.post("/articles/:articleid/mark_ham", &markAsHam); 74 router.post("/articles/:articleid/mark_spam", &markAsSpam); 75 router.get("/users/", &showListUsers); 76 router.get("/users/:user/", &showUser); 77 router.post("/users/:user/update", &updateUser); 78 router.post("/users/:user/delete", &deleteUser); 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 } 205 Info info; 206 info.settings = m_ctrl.settings; 207 info.groupname = req.params["groupname"]; 208 info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0; 209 m_ctrl.enumerateAllArticlesBackwards(info.groupname, info.page*info.articlesPerPage, info.articlesPerPage, (ref art){ info.articles ~= art; }); 210 info.articleCount = cast(int)m_ctrl.getAllArticlesCount(info.groupname); 211 info.pageCount = (info.articleCount-1)/info.articlesPerPage + 1; 212 213 res.render!("vibenews.admin.listarticles.dt", req, info); 214 } 215 216 void activateArticle(HTTPServerRequest req, HTTPServerResponse res) 217 { 218 auto artid = BsonObjectID.fromString(req.params["articleid"]); 219 m_ctrl.activateArticle(artid); 220 res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]); 221 } 222 223 void deactivateArticle(HTTPServerRequest req, HTTPServerResponse res) 224 { 225 auto artid = BsonObjectID.fromString(req.params["articleid"]); 226 m_ctrl.deactivateArticle(artid); 227 res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]); 228 } 229 230 void markAsSpam(HTTPServerRequest req, HTTPServerResponse res) 231 { 232 auto artid = BsonObjectID.fromString(req.params["articleid"]); 233 m_ctrl.markAsSpam(artid, true); 234 res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]); 235 } 236 237 void markAsHam(HTTPServerRequest req, HTTPServerResponse res) 238 { 239 auto artid = BsonObjectID.fromString(req.params["articleid"]); 240 m_ctrl.markAsSpam(artid, false); 241 res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]); 242 } 243 244 void showListUsers(HTTPServerRequest req, HTTPServerResponse res) 245 { 246 struct Info { 247 VibeNewsSettings settings; 248 enum itemsPerPage = 20; 249 UserInfo[] users; 250 int page; 251 int itemCount; 252 int pageCount; 253 } 254 255 Info info; 256 info.settings = m_ctrl.settings; 257 info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0; 258 string[userman.db.controller.Group.ID] groups; 259 m_ctrl.enumerateUsers(info.page*info.itemsPerPage, info.itemsPerPage, (ref user){ 260 info.users ~= getUserInfo(m_ctrl, user, groups); 261 }); 262 info.itemCount = cast(int)m_ctrl.getUserCount(); 263 info.pageCount = (info.itemCount-1)/info.itemsPerPage + 1; 264 265 res.render!("vibenews.admin.listusers.dt", req, info); 266 } 267 268 void showUser(HTTPServerRequest req, HTTPServerResponse res) 269 { 270 struct Info { 271 VibeNewsSettings settings; 272 UserInfo user; 273 } 274 User usr = m_ctrl.getUser(User.ID.fromString(req.params["user"])); 275 string[userman.db.controller.Group.ID] groups; 276 Info info; 277 info.settings = m_ctrl.settings; 278 info.user = getUserInfo(m_ctrl, usr, groups); 279 res.render!("vibenews.admin.edituser.dt", req, info); 280 } 281 282 void updateUser(HTTPServerRequest req, HTTPServerResponse res) 283 { 284 import std.algorithm.iteration : splitter; 285 286 auto user = m_ctrl.getUser(User.ID.fromString(req.params["user"])); 287 if (auto pv = "email" in req.form) { 288 validateEmail(*pv); 289 user.email = user.name = *pv; 290 } 291 if (auto pv = "fullName" in req.form) user.fullName = *pv; 292 if (auto pv = "groups" in req.form) { 293 user.groups.length = 0; 294 foreach (grp; (*pv).splitter(",").map!(g => authGroupPrefix ~ g.strip())) { 295 try user.groups ~= m_ctrl.getAuthGroupByName(grp).id; 296 catch (Exception) { 297 m_ctrl.userManController.addGroup(grp, "VibeNews authentication group"); 298 user.groups ~= m_ctrl.getAuthGroupByName(grp).id; 299 } 300 } 301 } 302 user.active = ("active" in req.form) !is null; 303 user.banned = ("banned" in req.form) !is null; 304 m_ctrl.updateUser(user); 305 306 res.redirect("/users/"); 307 } 308 309 void deleteUser(HTTPServerRequest req, HTTPServerResponse res) 310 { 311 m_ctrl.deleteUser(User.ID.fromString(req.params["user"])); 312 res.redirect("/users/"); 313 } 314 } 315 316 struct UserInfo { 317 User user; 318 alias user this; 319 ulong messageCount; 320 ulong deletedMessageCount; 321 string[] groupStrings; 322 } 323 324 private UserInfo getUserInfo(Controller ctrl, User user, ref string[userman.db.controller.Group.ID] groups) 325 { 326 UserInfo nfo; 327 nfo.user = user; 328 ctrl.getUserMessageCount(user.email, nfo.messageCount, nfo.deletedMessageCount); 329 foreach (g; user.groups) { 330 string grpname; 331 if (auto gd = g in groups) grpname = *gd; 332 else grpname = groups[g] = ctrl.getAuthGroup(g).name; 333 if (!grpname.startsWith(authGroupPrefix)) continue; 334 grpname = grpname[authGroupPrefix.length .. $]; 335 nfo.groupStrings ~= grpname; 336 } 337 return nfo; 338 }