1 /** 2 Web admin interface implementation 3 4 Copyright: © 2015-2017 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 userman.webadmin; 9 10 public import userman.api; 11 import userman.userman : validateUserName; 12 13 import vibe.core.log; 14 import vibe.http.router; 15 import vibe.textfilter.urlencode; 16 import vibe.utils.validation; 17 import vibe.web.auth; 18 import vibe.web.web; 19 20 import std.algorithm : min, max; 21 import std.array : appender; 22 import std.conv : to; 23 import std.exception; 24 import std.typecons : Nullable; 25 26 27 /** 28 Registers the routes for a UserMan web admin interface. 29 */ 30 void registerUserManWebAdmin(URLRouter router, UserManAPI api) 31 { 32 router.registerWebInterface(new UserManWebAdminInterface(api)); 33 } 34 35 /// private 36 @requiresAuth 37 @translationContext!TranslationContext 38 class UserManWebAdminInterface { 39 enum adminGroupName = "userman.admins"; 40 41 private { 42 UserManAPI m_api; 43 int m_entriesPerPage = 50; 44 SessionVar!(User.ID, "authUser") m_authUser; 45 SessionVar!(string, "authUserDisplayName") m_authUserDisplayName; 46 } 47 48 this(UserManAPI api) 49 { 50 m_api = api; 51 } 52 53 @noAuth 54 void getLogin(string redirect = "/", string _error = null) 55 { 56 bool first_user = m_api.users.count == 0; 57 string error = _error; 58 render!("userman.admin.login.dt", first_user, error, redirect); 59 } 60 61 @noAuth @errorDisplay!getLogin 62 void postLogin(string name, string password, string redirect = "/") 63 { 64 import std.algorithm.searching : canFind; 65 66 User.ID uid; 67 try uid = m_api.users.testLogin(name, password); 68 catch (Exception e) { 69 import std.encoding : sanitize; 70 logDebug("Error logging in: %s", e.toString().sanitize); 71 throw new Exception("Invalid user/email or password."); 72 } 73 74 auto user = m_api.users[uid].get(); 75 enforce(user.active, "The account is not yet activated."); 76 enforce(m_api.users[uid].getGroups().canFind(adminGroupName), "User is not an administrator."); 77 78 m_authUser = user.id; 79 m_authUserDisplayName = user.fullName; 80 .redirect(redirect); 81 } 82 83 @noAuth @errorDisplay!getLogin 84 void postInitialRegister(string username, ValidEmail email, string full_name, ValidPassword password, Confirm!"password" password_confirmation, string redirect = "/") 85 { 86 auto err = appender!string(); 87 enforceHTTP(m_api.settings.userNameSettings.validateUserName(err, username), HTTPStatus.badRequest, err.data); 88 89 enforceHTTP(m_api.users.count == 0, HTTPStatus.forbidden, "Cannot create initial admin account when other accounts already exist."); 90 try m_api.groups[adminGroupName].get(); 91 catch (Exception) m_api.groups.create(adminGroupName, "UserMan Administrators"); 92 93 auto uid = m_api.users.register(email, username, full_name, password); 94 m_api.groups[adminGroupName].members.add(uid); 95 m_authUser = uid; 96 .redirect(redirect); 97 } 98 99 @noAuth void getLogout() 100 { 101 terminateSession(); 102 redirect("/"); 103 } 104 105 106 // everything below requires authentication 107 @anyAuth: 108 109 void get(AuthInfo auth) 110 { 111 render!("userman.admin.index.dt"); 112 } 113 114 /*********/ 115 /* Users */ 116 /**************************************************************************/ 117 118 void getUsers(AuthInfo auth, int page = 1, string _error = null) 119 { 120 static struct Info { 121 User[] users; 122 int pageCount; 123 int page; 124 string error; 125 } 126 127 Info info; 128 info.page = page; 129 info.pageCount = ((m_api.users.count + m_entriesPerPage - 1) / m_entriesPerPage).to!int; 130 info.users = m_api.users.getRange((page-1) * m_entriesPerPage, m_entriesPerPage); 131 info.error = _error; 132 render!("userman.admin.users.dt", info); 133 } 134 135 @errorDisplay!getUsers 136 void postUsers(AuthInfo auth, string name, ValidEmail email, string full_name, ValidPassword password, Confirm!"password" password_confirmation) 137 { 138 auto err = appender!string(); 139 enforceHTTP(m_api.settings.userNameSettings.validateUserName(err, name), HTTPStatus.badRequest, err.data); 140 141 m_api.users.register(email, name, full_name, password); 142 redirect("users"); 143 } 144 145 @path("/users/multi") @errorDisplay!getUsers 146 void postMultiUserUpdate(AuthInfo auth, string action, HTTPServerRequest req, /*User.ID[] selection,*/ int page = 1) 147 { 148 import std.algorithm : map; 149 foreach (u; /*selection*/req.form.getAll("selection").map!(id => User.ID.fromString(id))) 150 performUserAction(u, action); 151 redirect(page > 1 ? "/users?page="~page.to!string : "/users"); 152 } 153 154 @path("/users/:user/") 155 void getUser(AuthInfo auth, User.ID _user, string _error = null) 156 { 157 import vibe.data.json : Json; 158 159 static struct Info { 160 User user; 161 Json[string] userProperties; 162 string error; 163 } 164 Info info; 165 info.user = m_api.users[_user].get(); 166 info.userProperties = m_api.users[_user].properties.get(); 167 info.error = _error; 168 render!("userman.admin.user.dt", info); 169 } 170 171 @path("/users/:user/") @errorDisplay!getUser 172 void postUser(AuthInfo auth, User.ID _user, string username, ValidEmail email, string full_name, bool active, bool banned) 173 { 174 auto err = appender!string(); 175 enforceHTTP(m_api.settings.userNameSettings.validateUserName(err, username), HTTPStatus.badRequest, err.data); 176 177 //m_api.users[_user].setName(username); // TODO! 178 m_api.users[_user].setEmail(email); 179 m_api.users[_user].setFullName(full_name); 180 m_api.users[_user].setActive(active); 181 m_api.users[_user].setBanned(banned); 182 redirect("/users/"~_user.toString~"/"); 183 } 184 185 @path("/users/:user/password") @errorDisplay!getUser 186 void postUserPassword(AuthInfo auth, User.ID _user, ValidPassword password, Confirm!"password" password_confirmation) 187 { 188 m_api.users[_user].setPassword(password); 189 redirect("/users/"~_user.toString~"/"); 190 } 191 192 @path("/users/:user/set_property") @errorDisplay!getUser 193 void postSetUserProperty(AuthInfo auth, User.ID _user, Nullable!string old_name, string name, string value) 194 { 195 import vibe.data.json : parseJson; 196 197 if (!old_name.isNull() && old_name != name) 198 m_api.users[_user].properties[old_name].remove(); 199 if (name.length) m_api.users[_user].properties[name].set(parseJson(value)); 200 redirect("./"); 201 } 202 203 /**********/ 204 /* Groups */ 205 /**************************************************************************/ 206 207 void getGroups(AuthInfo auth, long page = 1, string _error = null) 208 { 209 static struct Info { 210 Group[] groups; 211 long pageCount; 212 long page; 213 string error; 214 } 215 216 Info info; 217 info.page = page; 218 info.pageCount = (m_api.groups.count + m_entriesPerPage - 1) / m_entriesPerPage; 219 info.groups = m_api.groups.getRange((page-1) * m_entriesPerPage, m_entriesPerPage); 220 info.error = _error; 221 render!("userman.admin.groups.dt", info); 222 } 223 224 @errorDisplay!getGroups 225 void postGroups(AuthInfo auth, ValidGroupName name, string description) 226 { 227 m_api.groups.create(name, description); 228 redirect("/groups/"~name~"/"); 229 } 230 231 @path("/groups/multi") @errorDisplay!getGroups 232 void postMultiGroupUpdate(AuthInfo auth, string action, HTTPServerRequest req, /*User.ID[] selection,*/ int page = 1) 233 { 234 import std.algorithm : map; 235 foreach (g; /*selection*/req.form.getAll("selection")) 236 performGroupAction(g, action); 237 redirect(page > 1 ? "/groups?page="~page.to!string : "/groups"); 238 } 239 240 @path("/groups/:group/") 241 void getGroup(AuthInfo auth, string _group, string _error = null) 242 { 243 static struct Info { 244 Group group; 245 long memberCount; 246 string error; 247 } 248 Info info; 249 info.group = m_api.groups[_group].get(); 250 info.memberCount = m_api.groups[_group].members.count(); 251 info.error = _error; 252 render!("userman.admin.group.dt", info); 253 } 254 255 @path("/groups/:group/") @errorDisplay!getGroup 256 void postGroup(AuthInfo auth, string _group, string description) 257 { 258 m_api.groups[_group].setDescription(description); 259 redirect("/groups/"~_group~"/"); 260 } 261 262 /*@auth @path("/group/:group/set_property") @errorDisplay!getGroup 263 void postSetGroupProperty(AuthInfo auth, string _group, Nullable!string old_name, string name, string value) 264 { 265 import vibe.data.json : parseJson; 266 267 if (!old_name.isNull() && old_name != name) 268 m_api.groups.removeProperty(_group, old_name); 269 if (name.length) m_api.groups.setProperty(_group, name, parseJson(value)); 270 redirect("./"); 271 }*/ 272 273 @path("/groups/:group/members/") 274 void getGroupMembers(AuthInfo auth, string _group, long page = 1, string _error = null) 275 { 276 import std.algorithm : map; 277 import std.array : array; 278 279 static struct Info { 280 Group group; 281 User[] members; 282 long page; 283 long pageCount; 284 string error; 285 } 286 Info info; 287 info.group = m_api.groups[_group].get(); 288 info.page = page; 289 info.pageCount = ((m_api.groups.count + m_entriesPerPage - 1) / m_entriesPerPage).to!int; 290 info.members = m_api.groups[_group].members.getRange((page-1) * m_entriesPerPage, m_entriesPerPage) 291 .map!(id => m_api.users[id].get()) 292 .array; 293 info.error = _error; 294 render!("userman.admin.group.members.dt", info); 295 } 296 297 @path("/groups/:group/members/:user/remove") @errorDisplay!getGroupMembers 298 void postRemoveMember(AuthInfo auth, string _group, User.ID _user) 299 { 300 enforce(_group != adminGroupName || _user != auth.user.id, 301 "Cannot remove yourself from the admin group."); 302 m_api.groups[_group].members[_user].remove(); 303 redirect("/groups/"~_group~"/members/"); 304 } 305 306 @path("/groups/:group/members/") @errorDisplay!getGroupMembers 307 void postAddMember(AuthInfo auth, string _group, string username) 308 { 309 auto uid = m_api.users.getByName(username).id; 310 m_api.groups[_group].members.add(uid); 311 redirect("/groups/"~_group~"/members/"); 312 } 313 314 /************/ 315 /* Settings */ 316 /**************************************************************************/ 317 318 @path("/settings/") 319 void getSettings(AuthInfo auth, string _error = null) 320 { 321 struct Info { 322 string error; 323 UserManAPISettings settings; 324 } 325 326 Info info; 327 info.error = _error; 328 info.settings = m_api.settings; 329 render!("userman.admin.settings.dt", info); 330 } 331 332 @path("/settings/") @errorDisplay!getSettings 333 void posttSettings(AuthInfo auth) 334 { 335 // TODO! 336 redirect("/settings/"); 337 } 338 339 private void performUserAction(User.ID user, string action) 340 { 341 switch (action) { 342 default: throw new Exception("Unknown action: "~action); 343 case "activate": m_api.users[user].setActive(true); break; 344 case "deactivate": m_api.users[user].setActive(false); break; 345 case "ban": m_api.users[user].setBanned(true); break; 346 case "unban": m_api.users[user].setBanned(false); break; 347 case "delete": m_api.users[user].remove(); break; 348 case "sendActivation": 349 auto email = m_api.users[user].get().email; 350 m_api.users.resendActivation(email); 351 break; 352 } 353 } 354 355 private void performGroupAction(string group, string action) 356 { 357 switch (action) { 358 default: throw new Exception("Unknown action: "~action); 359 case "delete": 360 enforce(group != adminGroupName, "Cannot remove admin group."); 361 m_api.groups[group].remove(); 362 break; 363 } 364 } 365 366 @noRoute AuthInfo authenticate(HTTPServerRequest req, HTTPServerResponse res) 367 @trusted { 368 if (m_authUser == User.ID.init) { 369 redirect("/login?redirect="~req.path.urlEncode); 370 return AuthInfo.init; 371 } else { 372 return AuthInfo(m_api.users[m_authUser].get()); 373 } 374 } 375 } 376 377 private struct AuthInfo { 378 User user; 379 } 380 381 private struct TranslationContext { 382 import std.meta : AliasSeq; 383 alias languages = AliasSeq!("en_US", "de_DE"); 384 //mixin translationModule!"userman"; 385 } 386 387 struct ValidGroupName { 388 string m_value; 389 390 @disable this(); 391 392 private this(string value) { m_value = value; } 393 394 static Nullable!ValidGroupName fromStringValidate(string str, string* err) 395 { 396 import vibe.utils.validation : validateIdent; 397 import std.algorithm : splitter; 398 import std.array : appender; 399 400 // work around disabled default construction 401 auto ret = Nullable!ValidGroupName(ValidGroupName(null)); 402 ret.nullify(); 403 404 if (str.length < 1) { 405 *err = "Group names must not be empty."; 406 return ret; 407 } 408 auto errapp = appender!string; 409 foreach (p; str.splitter(".")) { 410 if (!validateIdent(errapp, p)) { 411 *err = errapp.data; 412 return ret; 413 } 414 } 415 416 ret = ValidGroupName(str); 417 return ret; 418 } 419 420 string toString() const { return m_value; } 421 422 alias toString this; 423 }