001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.users; 019 020import com.fasterxml.jackson.annotation.JsonView; 021import io.stallion.Context; 022import io.stallion.assets.*; 023import io.stallion.dataAccess.filtering.FilterChain; 024import io.stallion.dataAccess.filtering.Pager; 025import io.stallion.exceptions.*; 026import io.stallion.requests.validators.SafeMerger; 027import io.stallion.restfulEndpoints.*; 028import io.stallion.settings.Settings; 029import io.stallion.templating.TemplateRenderer; 030import io.stallion.utils.Sanitize; 031import io.stallion.utils.json.RestrictedViews; 032import org.apache.commons.lang3.StringUtils; 033 034import javax.ws.rs.*; 035 036import java.io.UnsupportedEncodingException; 037import java.net.URL; 038import java.net.URLEncoder; 039import java.util.Map; 040 041import static io.stallion.utils.Literals.*; 042import static io.stallion.Context.*; 043 044@Path("/st-users") 045public class UsersApiResource implements EndpointResource { 046 047 public static void register() { 048 if (Settings.instance().getUsers().getEnableDefaultEndpoints()) { 049 EndpointsRegistry.instance().addResource("", new UsersApiResource()); 050 051 /* 052 DefinedBundle.register(new DefinedBundle( 053 "userAdminStylesheets", ".css", 054 055 new BundleFile().setPluginName("stallion").setLiveUrl("admin/admin.css"), 056 new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage.css") 057 )); 058 DefinedBundle.register(new DefinedBundle( 059 "userAdminJavascripts", ".js", 060 new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage.js"), 061 new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-table-riot.tag").setProcessor("riot") 062 )); 063 064 065 BundleRegistry.instance().register( 066 new CompiledBundle("user-admin-vue", 067 new ResourceBundleFile("stallion", "admin/admin.css"), 068 new ResourceBundleFile("stallion", "admin/users-manage.css"), 069 new ResourceBundleFile("stallion", "vendor/vue.min.js", "vendor/vue.js"), 070 new ResourceBundleFile("stallion", "vendor/vue-router.min.js", "vendor/vue-router.js"), 071 new VueResourceBundleFile("stallion", "admin/*.vue"), 072 new ResourceBundleFile("stallion", "admin/users-manage-v2.js") 073 ) 074 ); 075 076*/ 077 /* 078 DefinedBundle.register( 079 "user-admin-vue", 080 new BundleFile().setPluginName("stallion").setLiveUrl("admin/admin.css"), 081 new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage.css"), 082 new BundleFile().setPluginName("stallion").setLiveUrl("vendor/vue.min.js").setDebugUrl("vendor/vue.js"), 083 new BundleFile().setPluginName("stallion").setLiveUrl("vendor/vue-router.min.js").setDebugUrl("vendor/vue-router.js"), 084 new VueBundleFile().setPluginName("stallion").setLiveUrl("admin/users-table.vue"), 085 new VueBundleFile().setPluginName("stallion").setLiveUrl("admin/users-edit.vue"), 086 new BundleFile().setPluginName("stallion").setLiveUrl("admin/users-manage-v2.js") 087 ); */ 088 } 089 } 090 091 @GET 092 @Path("/login") 093 @Produces("text/html") 094 public Object loginScreen(@QueryParam("email") String email) { 095 URL url = getClass().getResource("/templates/public/login.jinja"); 096 email = or(email, ""); 097 Map ctx = null; 098 try { 099 ctx = map( 100 val("allowReset", settings().getUsers().getPasswordResetEnabled()), 101 val("allowRegister", settings().getUsers().getNewAccountsAllowCreation()), 102 val("returnUrl", URLEncoder.encode((or(request().getParameter("stReturnUrl"), "")).replace("\"", ""), "UTF-8")), 103 val("email", Sanitize.escapeHtmlAttribute(email))); 104 } catch (UnsupportedEncodingException e) { 105 throw new RuntimeException(e); 106 } 107 String html = TemplateRenderer.instance().renderTemplate(url.toString(), ctx); 108 return html; 109 } 110 111 @POST 112 @Path("/valet-login") 113 @Produces("application/json") 114 @MinRole(Role.MEMBER) 115 public Object valetLogin(@BodyParam("targetUser") Object userKey) { 116 if (StringUtils.isNumeric(userKey.toString())) { 117 UserController.instance().valetLoginIfAllowed(Long.parseLong(userKey.toString())); 118 } else { 119 UserController.instance().valetLoginIfAllowed(userKey.toString()); 120 } 121 return true; 122 } 123 124 @GET 125 @Path("/logoff") 126 @Produces("text/html") 127 public Object logoff() { 128 UserController.instance().logoff(); 129 throw new RedirectException(Settings.instance().getUsers().getLoginPage(), 302); 130 } 131 132 @POST 133 @JsonView(RestrictedViews.Owner.class) 134 @Produces("application/json") 135 @Path("/submit-login") 136 public Object login(@BodyParam("username") String username, @BodyParam("password") String password, @BodyParam(value = "rememberMe", allowEmpty = true) Boolean rememberMe) { 137 return UserController.instance().loginUser(username, password, rememberMe); 138 } 139 140 @GET 141 @Produces("text/html") 142 @Path("/register") 143 @MinRole(Role.ANON) 144 public String registerPage() { 145 if (!settings().getUsers().getNewAccountsAllowCreation()) { 146 throw new ClientException("The default new account creation endpoint is not enabled for this site."); 147 } 148 Map ctx = map(); 149 return TemplateRenderer.instance().renderTemplate("stallion:/public/register.jinja", ctx); 150 } 151 152 @POST 153 @Produces("application/json") 154 @JsonView(RestrictedViews.Member.class) 155 @Path("/do-register") 156 public Object doRegister(@BodyParam("displayName") String displayName, @BodyParam("username") String email, @BodyParam("password") String password, @BodyParam("passwordConfirm") String passwordConfirm, @BodyParam(value = "returnUrl", allowEmpty = true) String returnUrl) { 157 if (!settings().getUsers().getNewAccountsAllowCreation()) { 158 throw new ClientException("The default new account creation endpoint is not enabled for this site."); 159 } 160 IUser existing = UserController.instance().forEmail(email); 161 if (existing != null) { 162 throw new ClientException("A user with that email address already exists."); 163 } 164 User user = new User() 165 .setDisplayName(displayName) 166 .setUsername(email) 167 .setRole(Role.valueOf(settings().getUsers().getNewAccountsRole().toUpperCase())) 168 .setEmail(email); 169 170 if (!settings().getUsers().getNewAccountsRequireValidEmail() && settings().getUsers().getNewAccountsAutoApprove() == true) { 171 user.setApproved(true); 172 } 173 Boolean requireValidEmail = false; 174 if (settings().getUsers().getNewAccountsRequireValidEmail()) { 175 requireValidEmail = true; 176 } 177 UserController.instance().hydratePassword(user, password, passwordConfirm); 178 IUser u = UserController.instance().createUser(user); 179 UserController.instance().addSessionCookieForUser(user, true); 180 if (requireValidEmail) { 181 UserController.instance().sendEmailVerifyEmail(user, or(returnUrl, "")); 182 } 183 return map(val("user", u), val("requireValidEmail", requireValidEmail)); 184 } 185 186 @POST 187 @Produces("application/json") 188 @JsonView(RestrictedViews.Member.class) 189 @MinRole(Role.ADMIN) 190 @Path("/admin-create-user") 191 public Object adminCreateUser(@ObjectParam User newUser) { 192 if (empty(newUser.getEmail())) { 193 newUser.setEmail(newUser.getUsername()); 194 } else if (empty(newUser.getUsername())) { 195 newUser.setUsername(newUser.getEmail()); 196 } 197 IUser user = SafeMerger.with() 198 .nonEmpty("email", "username", "displayName", "role") 199 .optional("familyName", "givenName") 200 .merge(newUser); 201 IUser existing = UserController.instance().forEmail(user.getEmail()); 202 if (existing != null) { 203 throw new ClientException("A user with that email address already exists."); 204 } 205 206 user.setApproved(true); 207 UserController.instance().createUser(user); 208 return user; 209 } 210 211 212 213 @POST 214 @Path("/send-verify-email") 215 @Produces("text/html") 216 public Object sendVerifyEmail(@BodyParam("email") String email, @BodyParam(value = "returnUrl", allowEmpty = true) String returnUrl) { 217 UserController.instance().sendEmailVerifyEmail(email, returnUrl); 218 return true; 219 } 220 221 @GET 222 @Path("/verify-email") 223 @Produces("text/html") 224 public Object verifyEmailAddress( @QueryParam("email") String email, @QueryParam("returnUrl") String returnUrl, @QueryParam("alreadySent") Boolean alreadySent) { 225 226 email = or(email, Context.getUser().getEmail()); 227 alreadySent = or(alreadySent, false); 228 Map ctx = map(val("email", Sanitize.stripAll(email)), val("alreadySent", alreadySent)); 229 String html = TemplateRenderer.instance().renderTemplate("stallion:/public/verify-email-address.jinja", ctx); 230 return html; 231 } 232 233 @GET 234 @Path("/verify-email-address") 235 @Produces("text/html") 236 public Object verifyEmailAddress(@QueryParam("verifyToken") String verifyToken, @QueryParam("email") String email, @QueryParam("returnUrl") String returnUrl) { 237 238 // For security, only verify an email address if we are logged in as that user 239 boolean requiresLogin = false; 240 241 if (empty(getUser().getId())) { 242 requiresLogin = true; 243 } else { 244 IUser associatedUser = UserController.instance().forEmail(email); 245 if (associatedUser == null || !associatedUser.getId().equals(getUser().getId())) { 246 requiresLogin = true; 247 } 248 } 249 if (requiresLogin) { 250 try { 251 String loginUrl = settings().getUsers().getFullLoginUrl() + "?email=" + email + 252 "&returnUrl=" + URLEncoder.encode(request().requestUrl(), "UTF-8"); 253 throw new RedirectException(loginUrl, 302); 254 } catch (UnsupportedEncodingException e) { 255 throw new RuntimeException(e); 256 } 257 } 258 259 260 Map<String, Object> ctx = map(val("verified", false), val("verificationFailed", false)); 261 if (!empty(verifyToken) && !empty(email)) { 262 boolean verified = UserController.instance().verifyEmailVerifyToken(email, verifyToken); 263 if (verified) { 264 UserController.instance().markEmailVerified(email, verifyToken); 265 ctx.put("verified", true); 266 } else { 267 ctx.put("verificationFailed", true); 268 } 269 } 270 if (empty(email) && !empty(Context.getUser().getEmail())) { 271 email = Context.getUser().getEmail(); 272 } 273 IUser user = UserController.instance().forUniqueKey("email", email); 274 if (user.getEmailVerified()) { 275 ctx.put("verified", true); 276 if (!user.getApproved()) { 277 ctx.put("requiresApproval", true); 278 } 279 280 } 281 282 returnUrl = or(returnUrl, Settings.instance().getSiteUrl()); 283 ctx.put("returnUrl", Sanitize.stripAll(returnUrl)); 284 ctx.put("email", Sanitize.stripAll(email)); 285 URL url = getClass().getResource("/templates/public/verify-email-address.jinja"); 286 String html = TemplateRenderer.instance().renderTemplate(url.toString(), ctx); 287 return html; 288 } 289 290 291 292 @GET 293 @Path("/reset-password") 294 @Produces("text/html") 295 public Object resetPassword(@QueryParam("resetToken") String resetToken, @QueryParam("email") String email, @QueryParam("returnUrl") String returnUrl) { 296 Map<String, Object> ctx = map(val("email", email), val("tokenVerified", false)); 297 if (!empty(resetToken) && !empty(email)) { 298 boolean verified = UserController.instance().verifyPasswordResetToken(email, resetToken); 299 if (verified) { 300 ctx.put("tokenVerified", verified); 301 } 302 } 303 URL url = getClass().getResource("/templates/public/reset-password.jinja"); 304 String html = TemplateRenderer.instance().renderTemplate(url.toString(), ctx); 305 return html; 306 } 307 308 309 @POST 310 @Path("/send-reset-email") 311 @Produces("application/json") 312 public Object sendResetEmail(@BodyParam("email") String email, @BodyParam(value = "returnUrl", allowEmpty = true) String returnUrl) { 313 if (!settings().getUsers().getPasswordResetEnabled()) { 314 throw new ClientException("Password reset has been disabled. Please contact an administrator to reset your password."); 315 } 316 IUser user = UserController.instance().forEmail(email); 317 if (user == null) { 318 user = UserController.instance().forUsername(email); 319 } 320 UserController.instance().sendPasswordResetEmail(user, returnUrl); 321 return true; 322 } 323 324 325 @POST 326 @Path("/do-password-reset") 327 @Produces("application/json") 328 public Object doPasswordReset( 329 @BodyParam("resetToken") String resetToken, 330 @BodyParam("email") String email, 331 @BodyParam("password") String password, 332 @BodyParam("passwordConfirm") String passwordConfirm 333 ) { 334 335 IUser user = UserController.instance().changePassword(email, resetToken, password, passwordConfirm); 336 if (user == null) { 337 return false; 338 } 339 UserController.instance().addSessionCookieForUser(user, true); 340 return true; 341 } 342 343 /* 344 345 @POST 346 @Path("/create-new-account") 347 @Produces("application/json") 348 public Object createNewAccount(@BodyParam("displayName") String displayName, @BodyParam(value = "email", isEmail = true) String email, @BodyParam(value = "password", minLength = 6) String password, @BodyParam(value = "passwordConfirm", minLength = 6) String passwordConfirm) { 349 if (!settings().getUsers().getNewAccountsAllowCreation()) { 350 throw new ClientException("User creation is disabled for this application."); 351 } 352 353 String domain = StringUtils.split(email, "@", 2)[1]; 354 if (!empty(settings().getUsers().getNewAccountsDomainRestricted())) { 355 if (!settings().getUsers().getNewAccountsDomainRestricted().equals(domain)) { 356 throw new ClientException("You can only register email accounts from domain " + domain); 357 } 358 } 359 360 IUser user = UserController.instance().forEmail(email); 361 if (user == null) { 362 throw new ClientException("User with that email address already exists."); 363 } 364 user = new User(); 365 user 366 .setDisplayName(displayName) 367 .setEmail(email); 368 369 370 if (!empty(settings().getUsers().getNewAccountsRole())) { 371 user.setRole(Role.valueOf(settings().getUsers().getNewAccountsRole())); 372 } 373 374 if (settings().getUsers().getNewAccountsAutoApprove()) { 375 user.setApproved(true); 376 } 377 378 UserController.instance().hydratePassword(user, password, passwordConfirm); 379 UserController.instance().createUser(user); 380 381 Map<String, Object> ctx = map(val("verifyEmailSent", false), val("pendingAdminApproval", false)); 382 if (!settings().getUsers().getNewAccountsAutoApprove()) { 383 ctx.put("pendingAdminApproval", true); 384 } 385 386 if (settings().getUsers().getNewAccountsRequireValidEmail()) { 387 UserController.instance().sendEmailVerifyEmail(user, ""); 388 ctx.put("verifyEmailSent", true); 389 } 390 391 392 UserController.instance().addSessionCookieForUser(user, true); 393 394 return map(val("verifyEmailSent", true), val("pendingAdminApproval", false)); 395 396 397 } 398 399 */ 400 401 @GET 402 @Path("/current-user-info") 403 @JsonView(RestrictedViews.Owner.class) 404 public Object currentUserInfo() { 405 if (Context.getUser() == null || empty(Context.getUser().getId())) { 406 throw new ClientException("You are not logged in", 401); 407 } 408 return Context.getUser(); 409 } 410 411 412 /****** 413 * 414 * ADMIN Endpoints 415 * 416 */ 417 418 419 420 @GET 421 @Path("/manage") 422 @MinRole(Role.ADMIN) 423 @Produces("text/html") 424 public String manageUsers2() { 425 426 response().getMeta().setTitle("Manage Users"); 427 Map<String, Object> ctx = map(); 428 return TemplateRenderer.instance().renderTemplate("stallion:admin/admin-users2.jinja", ctx); 429 } 430 431 432 @GET 433 @Path("/users-screen") 434 @MinRole(Role.ADMIN) 435 @Produces("application/json") 436 public Map manageUsersScreen() { 437 Map<String, Object> ctx = map(); 438 return ctx; 439 } 440 441 @GET 442 @Path("/users-table") 443 @MinRole(Role.ADMIN) 444 @Produces("application/json") 445 @JsonView(RestrictedViews.Owner.class) 446 public Pager usersTable(@QueryParam("page") Integer page, @QueryParam("withDeleted") Boolean withDeleted) { 447 Map<String, Object> ctx = map(); 448 if (page == null || page < 1) { 449 page = 1; 450 } 451 withDeleted = or(withDeleted, false); 452 FilterChain chain = UserController.instance().filterChain(); 453 if (withDeleted) { 454 chain = chain.includeDeleted(); 455 } 456 Pager pager = chain.sort("email", "ASC").pager(page, 100); 457 return pager; 458 } 459 460 461 @GET 462 @Path("/view-user/:userId") 463 @MinRole(Role.ADMIN) 464 @Produces("application/json") 465 @JsonView(RestrictedViews.Owner.class) 466 public IUser viewUser(@PathParam("userId") Long userId) { 467 IUser user = UserController.instance().forIdWithDeleted(userId); 468 if (user == null) { 469 throw new io.stallion.exceptions.NotFoundException("User not found"); 470 } 471 return user; 472 } 473 474 @POST 475 @Path("/update-user/:userId") 476 @MinRole(Role.ADMIN) 477 @Produces("application/json") 478 @JsonView(RestrictedViews.Owner.class) 479 public IUser updateUser(@PathParam("userId") Long userId, @ObjectParam(targetClass=User.class) User updatedUser) { 480 IUser user = UserController.instance().forIdWithDeleted(userId); 481 if (user == null) { 482 throw new io.stallion.exceptions.NotFoundException("User not found"); 483 } 484 if (!updatedUser.getEmail().equals(user.getEmail())) { 485 IUser existing = UserController.instance().forEmail(updatedUser.getEmail()); 486 if (existing != null) { 487 throw new ClientException("User with email " + updatedUser.getEmail() + " already exists"); 488 } 489 user.setEmailVerified(false); 490 } 491 if (!updatedUser.getUsername().equals(user.getUsername())) { 492 IUser existing = UserController.instance().forUsername(updatedUser.getUsername()); 493 if (existing != null) { 494 throw new ClientException("User with username " + updatedUser.getUsername() + " already exists"); 495 } 496 } 497 user 498 .setDisplayName(updatedUser.getDisplayName()) 499 .setUsername(updatedUser.getUsername()) 500 .setEmail(updatedUser.getEmail()) 501 .setRole(updatedUser.getRole()) 502 .setFamilyName(updatedUser.getFamilyName()) 503 .setGivenName(updatedUser.getGivenName()); 504 505 UserController.instance().save(user); 506 507 508 return user; 509 } 510 511 @POST 512 @Path("/toggle-user-disabled/:userId") 513 @MinRole(Role.ADMIN) 514 @Produces("application/json") 515 @JsonView(RestrictedViews.Owner.class) 516 public Object toggleDisableUser(@PathParam("userId") Long userId, @BodyParam("disabled") boolean disabled) { 517 IUser user = UserController.instance().forIdWithDeleted(userId); 518 if (user == null) { 519 throw new io.stallion.exceptions.NotFoundException("User not found"); 520 } 521 user.setDisabled(disabled); 522 UserController.instance().save(user); 523 return true; 524 } 525 526 @POST 527 @Path("/toggle-user-approved/:userId") 528 @MinRole(Role.ADMIN) 529 @Produces("application/json") 530 @JsonView(RestrictedViews.Owner.class) 531 public Object toggleUserApproved(@PathParam("userId") Long userId, @BodyParam("approved") boolean approved) { 532 IUser user = UserController.instance().forIdWithDeleted(userId); 533 if (user == null) { 534 throw new io.stallion.exceptions.NotFoundException("User not found"); 535 } 536 user.setApproved(approved); 537 UserController.instance().save(user); 538 return true; 539 } 540 541 @POST 542 @Path("/toggle-user-deleted/:userId") 543 @MinRole(Role.ADMIN) 544 @Produces("application/json") 545 @JsonView(RestrictedViews.Owner.class) 546 public Object toggleUserDeleted(@PathParam("userId") Long userId, @BodyParam("deleted") boolean deleted) { 547 IUser user = UserController.instance().forIdWithDeleted(userId); 548 if (user == null) { 549 throw new io.stallion.exceptions.NotFoundException("User not found"); 550 } 551 if (deleted == true) { 552 UserController.instance().softDelete(user); 553 } else { 554 user.setDeleted(deleted); 555 UserController.instance().save(user); 556 } 557 558 return true; 559 } 560 561 @POST 562 @Path("/force-password-reset/:userId") 563 @MinRole(Role.ADMIN) 564 @Produces("application/json") 565 @JsonView(RestrictedViews.Owner.class) 566 public Object triggerPasswordReset(@PathParam("userId") Long userId) { 567 IUser user = UserController.instance().forIdWithDeleted(userId); 568 if (user == null) { 569 throw new io.stallion.exceptions.NotFoundException("User not found"); 570 } 571 user.setBcryptedPassword(""); 572 UserController.instance().save(user); 573 UserController.instance().sendPasswordResetEmail(user, ""); 574 return true; 575 } 576 577 @Path("/selenium/get-reset-token") 578 @GET 579 @Produces("application/json") 580 public Map getResetToken(@QueryParam("email") String email, @QueryParam("secret") String secret) { 581 if (!Settings.instance().getHealthCheckSecret().equals(secret)) { 582 throw new ClientException("Invalid or missing ?secret= query param. Secret must equal the healthCheckSecret in settings."); 583 } 584 if (!email.startsWith("selenium+resettest+") || !email.endsWith("@stallion.io")) { 585 throw new ClientException("Invalid email address. Must be a stallion selenium email."); 586 } 587 IUser user = UserController.instance().forEmail(email); 588 String token = UserController.instance().makeEncryptedToken(user, "reset", user.getResetToken()); 589 return map(val("resetToken", token)); 590 } 591 592}