/*
* Copyright 2012 Nodeable Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.streamreduce.rest.resource.admin;
import com.google.common.collect.ImmutableMap;
import com.streamreduce.InvalidUserAliasException;
import com.streamreduce.core.model.Account;
import com.streamreduce.core.model.User;
import com.streamreduce.core.service.UserService;
import com.streamreduce.core.service.exception.UserNotFoundException;
import com.streamreduce.rest.dto.response.ConstraintViolationExceptionResponseDTO;
import com.streamreduce.rest.resource.ErrorMessages;
import com.streamreduce.util.SecurityUtil;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import net.sf.json.JSONObject;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Methods in this class should not be publically available. They are for internal and super-user use only!
*/
@Component
@Path("admin/user/verify")
public class AdminUserVerificationResource extends AbstractAdminResource {
@Autowired
UserService userService;
/**
* Helper method that is a simple test to confirm that the key and userId are valid -- not required to use before the actually reset process takes place, as
* the check will be performed again in that method.
*
* @param key - the key to match
* @param userId - id if the User to load and test the key against
* @return - the username (email address) associated with the key
* @resource.representation.404 returned if the user is not found
* @resource.representation.500 returned if invalid params are provided or if the key does not match what
* we have for that User
*/
@GET
@Path("password/{key}/{userId}")
public Response verifyPasswordResetIdentity(@PathParam("key") String key,
@PathParam("userId") String userId) {
if (isEmpty(key) || isEmpty(userId)) {
return error("Path params missing", Response.status(Response.Status.BAD_REQUEST));
}
User user;
try {
user = applicationManager.getUserService().getUserById(new ObjectId(userId));
} catch (UserNotFoundException e) {
return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
}
// verify the secret key matches
if (!(user.getSecretKey().equals(key))) {
logger.debug("Password identity confirmation failure: userId and secret key pair does NOT match.");
return error("Password Identity Confirmation Failed", Response.status(Response.Status.BAD_REQUEST));
}
// send the password back to the client.
return Response
.ok()
.entity(user.getUsername())
.build();
}
/**
* User is confirming the password reset confirmation link and selecting a new password. The new password is passed
* in the JSON payload. The key and userId combinations are validated here.
*
* @param key - auto-generated key from the email
* @param userId - userId
* @param json - the new password
* @return - http status code
* @resource.representation.201 returned if a new password was created for the User
* @resource.representation.404 returned if the user is not found
* @resource.representation.500 returned if invalid params are provided or if the key does not match
*/
@POST
@Path("password/{key}/{userId}")
@Consumes(MediaType.APPLICATION_JSON)
public Response completePasswordResetProcess(@PathParam("key") String key,
@PathParam("userId") String userId,
JSONObject json) {
User user;
try {
user = applicationManager.getUserService().getUserById(new ObjectId(userId));
} catch (UserNotFoundException e) {
return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
}
// verify the secret key matches
if (!(user.getSecretKey().equals(key))) {
logger.debug("Password change confirmation failure: userId and secret key pair does NOT match.");
return error("Password Change Confirmation Failed", Response.status(Response.Status.BAD_REQUEST));
}
String password = getJSON(json, "password");
// validate password
if (!SecurityUtil.isValidPassword(password)) {
return error(ErrorMessages.INVALID_PASSWORD_ERROR, Response.status(Response.Status.BAD_REQUEST));
}
// we want to make this a one time thing, blow away the secret key
user.setSecretKey(null);
// update the password in the db
user.setPassword(password);
applicationManager.getUserService().updateUser(user);
// send the password back to the client.
return Response
.status(Response.Status.CREATED)
.build();
}
/**
* Helper method that simply confirms the key and accountId association is valid -- this is not a required step for
* a newly invited user.
*
* @param inviteKey - the invite key from the email
* @param accountId - the accountId of the User
* @return - http status code and the username (email) of user
* @resource.representation.200 returned if the values match
* @resource.representation.404 returned if the user is not found
* @resource.representation.406 returned if the user is already activated
* @resource.representation.500 returned if invalid params are provided or if the key does not match
*/
@GET
@Path("invite/{inviteKey}/{accountId}")
public Response verifyUserInviteIdentity(@PathParam("inviteKey") String inviteKey,
@PathParam("accountId") String accountId) {
// check for missing required values
if (isEmpty(inviteKey) || isEmpty(accountId)) {
return error("Required path params missing", Response.status(Response.Status.BAD_REQUEST));
}
User user;
try {
user = applicationManager.getUserService().getUserFromInvite(inviteKey, accountId);
// this account is already active!
if (user.getUserStatus().equals(User.UserStatus.ACTIVATED)) {
return error("User is already activated", Response.status(Response.Status.NOT_ACCEPTABLE));
}
} catch (UserNotFoundException e) {
return error("Invalid Invite " + inviteKey + ":" + accountId, Response.status(Response.Status.NOT_FOUND));
}
// user created, they can now login
return Response
.ok()
.entity(user.getUsername())
.build();
}
/**
* Resource to complete the setup process of an invited user. This moves the user from a pending state to a fully
* built state. The user can only be associated with the Account they were invited to join.
*
* @param inviteKey - auto-generated key contained in the email
* @param accountId - the accountId (invites require an Account) used only for verification
* @param json - invited User DTO with required fields such as password, alias and fullname
* @return - http status code
* @resource.representation.301 returned if successful
* @resource.representation.404 returned if the user is not found
* @resource.representation.406 returned if the user is already activated or if the alias is already taken
* @resource.representation.500 returned if invalid params are provided
*/
@POST
@Path("invite/{inviteKey}/{accountId}")
@Consumes(MediaType.APPLICATION_JSON)
public Response completeUserInviteProcess(@PathParam("inviteKey") String inviteKey,
@PathParam("accountId") String accountId,
JSONObject json) {
String password = getJSON(json, "password");
String alias = getJSON(json, "alias");
String fullname = getJSON(json, "fullname");
if (isEmpty(password) || isEmpty(alias) || isEmpty(fullname)) {
return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
}
User user;
try {
// validate password
if (!SecurityUtil.isValidPassword(password)) {
return error(ErrorMessages.INVALID_PASSWORD_ERROR, Response.status(Response.Status.BAD_REQUEST));
}
user = userService.getUserFromInvite(inviteKey, accountId);
// this account is already active!
// FIXME: this status code sucks here
if (user.getUserStatus().equals(User.UserStatus.ACTIVATED)) {
return error("User is already activated", Response.status(Response.Status.NOT_ACCEPTABLE));
}
// verify alias is unique
if (!userService.isAliasAvailable(user.getAccount(), alias)) {
return error("Alias is not available", Response.status(Response.Status.NOT_ACCEPTABLE));
}
user.setPassword(password);
user.setFullname(fullname);
user.setAlias(alias);
// we already know their company..so no need to set it
// user can login
user.setUserLocked(false);
// we want to make this a one time thing, blow away the secret key
user.setSecretKey(null);
userService.createUser(user);
} catch (UserNotFoundException e) {
return error("Invalid Invite " + inviteKey + ":" + accountId, Response.status(Response.Status.NOT_FOUND));
} catch (InvalidUserAliasException e) {
ConstraintViolationExceptionResponseDTO dto = new ConstraintViolationExceptionResponseDTO();
dto.setViolations(ImmutableMap.of("alias", e.getMessage()));
return Response.status(Response.Status.BAD_REQUEST).entity(dto).build();
}
// user created, they can now login
return Response
.status(Response.Status.CREATED)
.entity(user.getUsername())
.build();
}
/**
* A Helper method that can be used to verify the email sign-up key and userId are a valid combination. This is not
* a required step during the sign-up process as validation will once again be performed in the POST method.
*
* @param signupKey - unique to a user and sent via email
* @param userId - the userId
* @return - http status code and the username (email) of user
* @resource.representation.200 returned if the values match
* @resource.representation.404 returned if the user is not found
* @resource.representation.406 returned if the user is already activated
* @resource.representation.500 returned if invalid params are provided or if the key does not match
*/
@GET
@Path("signup/{signupKey}/{userId}")
public Response verifyUserSignupIdentity(@PathParam("signupKey") String signupKey,
@PathParam("userId") String userId) {
// check for missing required values
if (isEmpty(signupKey) || isEmpty(userId)) {
return error("Required path params missing", Response.status(Response.Status.BAD_REQUEST));
}
User user;
try {
user = applicationManager.getUserService().getUserFromSignupKey(signupKey, userId);
// this account is already active!
if (user.getUserStatus().equals(User.UserStatus.ACTIVATED)) {
return error("User is already activated", Response.status(Response.Status.NOT_ACCEPTABLE));
}
} catch (UserNotFoundException e) {
return error("Invalid Signup " + signupKey + ":" + userId, Response.status(Response.Status.NOT_FOUND));
}
return Response
.ok()
.entity(user.getUsername())
.build();
}
/**
* A new user is completing the sign-up process. This moves the user from a pending state to a complete state. A confirmation
* email should be sent out once this process is complete.
*
* @param signupKey - auto-generated key from the email
* @param userId - the userId
* @param json - invited User DTO with required fields such as password, alias, accountName and fullname
* @return - http status code and username as the created entity on success
* @resource.representation.301 returned if successful
* @resource.representation.404 returned if the user is not found
* @resource.representation.406 returned if the user is already activated or if the alias is already taken
* @resource.representation.500 returned if invalid params are provided
*/
@POST
@Path("signup/{signupKey}/{userId}")
@Consumes(MediaType.APPLICATION_JSON)
public Response completeUserSignupProcess(@PathParam("signupKey") String signupKey,
@PathParam("userId") String userId,
JSONObject json) {
String password = getJSON(json, "password");
String alias = getJSON(json, "alias");
String fullname = getJSON(json, "fullname");
String accountName = getJSON(json, "accountName");
if (isEmpty(password) || isEmpty(alias) || isEmpty(fullname) || isEmpty(accountName)) {
return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
}
User user;
try {
// validate password
if (!SecurityUtil.isValidPassword(password)) {
return error(ErrorMessages.INVALID_PASSWORD_ERROR, Response.status(Response.Status.BAD_REQUEST));
}
user = userService.getUserFromSignupKey(signupKey, userId);
// this account is already active!
if (user.getUserStatus().equals(User.UserStatus.ACTIVATED)) {
return error("User is already activated", Response.status(Response.Status.NOT_ACCEPTABLE));
}
user.setPassword(password);
user.setFullname(fullname);
user.setAlias(alias); // no need to check, you are the first user
// user can now login, the account is "activated"
user.setUserLocked(false);
// we want to make this a one time thing, blow away the secret key
user.setSecretKey(null);
Account account = new Account.Builder()
.name(accountName)
.build();
// create the account and fire events
userService.createAccount(account);
user.setAccount(account);
// create the user and fire events
userService.createUser(user);
} catch (UserNotFoundException e) {
return error("Invalid Signup " + signupKey + ":" + userId, Response.status(Response.Status.NOT_FOUND));
} catch (InvalidUserAliasException e) {
ConstraintViolationExceptionResponseDTO dto = new ConstraintViolationExceptionResponseDTO();
dto.setViolations(ImmutableMap.of("alias", e.getMessage()));
return Response.status(Response.Status.BAD_REQUEST).entity(dto).build();
}
return Response
.status(Response.Status.CREATED)
.entity(user.getUsername()) // This should probably be some sort of response DTO but due to time this is what you'll get.
.build();
}
}