package com.github.windbender.resources;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
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.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.codec.binary.Base64;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.windbender.auth.SessionCurProj;
import com.github.windbender.auth.SessionUser;
import com.github.windbender.core.AcceptRequest;
import com.github.windbender.core.CurUser;
import com.github.windbender.core.LoginObject;
import com.github.windbender.core.MenuItem;
import com.github.windbender.core.ProjectMenuTO;
import com.github.windbender.core.ResetPWRequest;
import com.github.windbender.core.SetPWRequest;
import com.github.windbender.core.SignUpResponse;
import com.github.windbender.core.SignupRequest;
import com.github.windbender.core.UserTO;
import com.github.windbender.core.UserUpdate;
import com.github.windbender.core.VerifyRequest;
import com.github.windbender.core.VerifyResponse;
import com.github.windbender.dao.HibernateUserDAO;
import com.github.windbender.dao.InviteDAO;
import com.github.windbender.dao.ProjectDAO;
import com.github.windbender.dao.TokenDAO;
import com.github.windbender.dao.UserDAO;
import com.github.windbender.dao.UserProjectDAO;
import com.github.windbender.domain.Invite;
import com.github.windbender.domain.Project;
import com.github.windbender.domain.User;
import com.github.windbender.domain.UserProject;
import com.github.windbender.service.EmailService;
import com.sun.jersey.api.ConflictException;
import com.sun.jersey.api.NotFoundException;
import com.yammer.dropwizard.hibernate.UnitOfWork;
import com.yammer.metrics.annotation.Timed;
@Path("/users/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
Logger log = LoggerFactory.getLogger(UserResource.class);
private UserDAO ud;
private TokenDAO tokendao;
private EmailService emailService;
private ProjectDAO projectDAO;
private UserProjectDAO upDAO;
private InviteDAO inviteDAO;
public UserResource(UserDAO ud, TokenDAO tokendao,ProjectDAO projectDAO,UserProjectDAO upDAO,InviteDAO inviteDAO,
EmailService emailService) {
super();
this.ud = ud;
this.tokendao = tokendao;
this.emailService = emailService;
this.projectDAO = projectDAO;
this.upDAO = upDAO;
this.inviteDAO = inviteDAO;
}
@POST
@Timed
@Path("logout")
@Consumes(MediaType.APPLICATION_XML)
@UnitOfWork
public Response logout(@SessionUser User user, @Context HttpServletRequest request) {
log.info("attempting logout for user "+user.toString());
clearSession(request);
return Response.status(Response.Status.OK).build();
}
@GET
@Timed
@Path("menus")
@UnitOfWork
public List<MenuItem> getMenus(@SessionUser(required=false) User user, @SessionCurProj Project currentProject ) {
List<MenuItem> list = new ArrayList<MenuItem>();
if(user == null) {
return list;
}
if(currentProject == null) {
return list;
}
User u = this.ud.findById(user.getId());
Project p = this.projectDAO.findById(currentProject.getId());
boolean isAdmin = false;
if(p.getPrimaryAdmin().equals(u)) isAdmin = true;
List<UserProject> upl = this.upDAO.findByUserIdProjectId(u,p);
if(upl.size() > 0) {
UserProject up = upl.get(0);
if(up.getCanAdmin()) {
list.add(new MenuItem("admin","#/setup"));
}
if(up.getCanUpload() ) {
list.add(new MenuItem("upload","#/upload"));
}
if(up.getCanCategorize() || p.getPublicCategorize() ) {
list.add(new MenuItem("categorize","#/categorize"));
}
if(up.getCanReport() || p.getPublicReport() ) {
list.add(new MenuItem("report","#/report"));
}
if(up.getCanReport() || p.getPublicReport() ) {
list.add(new MenuItem("best of","#/bestof"));
}
} else if (isAdmin ) {
list.add(new MenuItem("admin","#/setup"));
list.add(new MenuItem("upload","#/upload"));
list.add(new MenuItem("categorize","#/categorize"));
list.add(new MenuItem("report","#/report"));
list.add(new MenuItem("best of","#/bestof"));
}
return list;
}
@GET
@Timed
@Path("lookup")
@UnitOfWork
public List<UserTO> lookupToo(@SessionUser User user, @QueryParam("text") String snippet,@QueryParam("p") String projectIdStr) {
if(snippet == null) throw new NotFoundException();
if(snippet.length() < 3) throw new NotFoundException();
Project project = null;
try {
Long projectId = null;
projectId = Long.parseLong(projectIdStr);
project = this.projectDAO.findById(projectId);
} catch(NumberFormatException nfe) {}
User u = this.ud.findByPortionOfEmailUsername(snippet);
if(u == null) return null;
// now remove it, if it's already in existence.
List<UserProject> current= this.upDAO.findByUserIdProjectId(u, project);
if(current.size() > 0) throw new NotFoundException();
ArrayList<UserTO> l = new ArrayList<UserTO>();
l.add(new UserTO(u));
return l;
}
@GET
@Timed
@Path("check")
@UnitOfWork
public Response checkExists(@QueryParam("username") String username) {
if(username == null) throw new NotFoundException("needs query param \"username\"");
if(username.length() < 4) throw new NotFoundException("username needs decent length > 3");
User u = ud.findByUsername(username);
if(u == null) {
throw new NotFoundException("user does not exist");
}
return Response.ok( MediaType.APPLICATION_JSON).build();
}
@GET
@Timed
@Path("myprojects")
@UnitOfWork
public List<Project> myProjects(@SessionUser User user) {
User u = this.ud.findById(user.getId());
if(u==null) throw new WebApplicationException();
List<Project> lp = this.projectDAO.findByPrimaryAdmin(u);
Set<Project> sp = new TreeSet<Project>();
if(lp!=null) sp.addAll(lp);
List<Project> outList = new ArrayList<Project>(sp);
return outList;
}
@GET
@Timed
@Path("projects")
@UnitOfWork
public List<ProjectMenuTO> projects(@SessionUser(required=false) User user) {
List<ProjectMenuTO> outList = new ArrayList<ProjectMenuTO>();
if(user!=null) {
User u = this.ud.findById(user.getId());
List<Project> lp = this.projectDAO.findByPrimaryAdmin(u);
Set<Project> sp = new TreeSet<Project>();
if(lp!=null) sp.addAll(lp);
List<UserProject> lup = this.upDAO.findAllByUser(u);
if(lup != null) {
for(UserProject up: lup) {
Project p = up.getProject();
sp.add(p);
}
}
for(Project p: sp) {
outList.add(new ProjectMenuTO(p,user));
}
}
return outList;
}
@POST
@Timed
@Path("currentProject")
@UnitOfWork
public Response currentProject(@SessionUser User user, @Context HttpServletRequest request, int project_id) {
log.info("attempting to set current project to "+project_id);
Project p = this.projectDAO.findById(project_id);
if(!userHasPrivsOn(user,p)) throw new WebApplicationException();
if(p != null)
request.getSession().setAttribute("current_project", p);
return Response.status(Response.Status.OK).build();
}
@GET
@Timed
@Path("currentProject")
@UnitOfWork
public Project currentProjectGet(@SessionUser(required=false) User user, @SessionCurProj Project currentProject) {
if(currentProject == null) return null;
Project cp = this.projectDAO.findById(currentProject.getId());
return cp;
}
private boolean userHasPrivsOn(User user, Project p) {
// TODO Auto-generated method stub
return true;
}
@GET
@Timed
@Path("getLoggedIn")
@UnitOfWork
public CurUser getLoggedInUser(@Context HttpServletRequest request) {
User user = (User) request.getSession().getAttribute("user");
if(user == null) return new CurUser(null);
String username = user.getUsername();
CurUser cu = new CurUser(username);
return cu;
}
@POST
@Timed
@Path("userupdate")
@UnitOfWork
public Response userupdate(@SessionUser User user, @Context HttpServletRequest request, UserUpdate updatedCurUser) {
int userId = user.getId();
User editUser = this.ud.findById(userId);
// email -- this should require a REVERIFICATION
this.ud.save(editUser);
clearSession(request);
loadAssociatedAndSetSessionWith(request, editUser);
loadStartingProject(request,editUser);
// there is an implicit SAVE on editUser
return Response.status(Response.Status.OK).build();
}
public static <T> T initializeAndUnproxy(T entity) {
if (entity == null) {
throw new
NullPointerException("Entity passed for initialization is null");
}
Hibernate.initialize(entity);
if (entity instanceof HibernateProxy) {
entity = (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
}
return entity;
}
@POST
@Timed
@Path("login")
@UnitOfWork
public Response login( @Valid LoginObject login, @Context HttpServletRequest request) {
log.info("attempting login for user "+login.getUsername());
if(ud.checkPassword(login.getUsername(), login.getPassword())) {
log.info("success");
User p = ud.findByUsername(login.getUsername());
if(p == null) {
log.info("fail");
clearSession(request);
return Response.status(Response.Status.FORBIDDEN).build();
}
boolean ver = false;
if(p.getVerified() != null) {
ver = p.getVerified().booleanValue();
}
if(ver) {
loadAssociatedAndSetSessionWith(request, p);
loadStartingProject(request,p);
return Response.status(Response.Status.OK).build();
} else {
log.info("fail not verified");
clearSession(request);
//throw new ForbiddenException("sorry that user is not yet verified");
return Response.status(Response.Status.FORBIDDEN).build();
}
} else {
log.info("fail");
clearSession(request);
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void clearSession(HttpServletRequest request) {
request.getSession().setAttribute("user", null);
request.getSession().setAttribute("current_project", null);
request.getSession().setAttribute("user_projects", null);
request.getSession().setAttribute("primary_admins", null);
}
private void loadStartingProject(HttpServletRequest request, User p) {
List<Project> primaryAdminProjects = (List<Project>) request.getSession().getAttribute("primary_admins");
Project currentProject = null;
if((primaryAdminProjects != null) && (primaryAdminProjects.size() > 0) ){
currentProject = primaryAdminProjects.get(0);
}
if(currentProject == null) {
List<UserProject> upl = (List<UserProject>) request.getSession().getAttribute("user_projects");
if((upl != null) && (upl.size() > 0) ) {
UserProject up = upl.get(0);
currentProject = this.projectDAO.findById(up.getProject().getId().intValue());
}
}
request.getSession().setAttribute("current_project", currentProject);
}
private void loadAssociatedAndSetSessionWith(HttpServletRequest request,
User p) {
request.getSession().setAttribute("user", p);
User u = this.ud.findById(p.getId());
List<Project> primaryAdminProjects = this.projectDAO.findByPrimaryAdmin(u);
request.getSession().setAttribute("primary_admins", primaryAdminProjects);
List<UserProject> upl = this.upDAO.findAllByUser(u);
request.getSession().setAttribute("user_projects", upl);
}
@POST
@Timed
@Path("logout")
@UnitOfWork
public Response logout(@SessionUser User User, @Context HttpServletRequest request, String req) {
log.info("attempting logout for user "+User.toString());
clearSession(request);
request.getSession().invalidate();
return Response.status(Response.Status.OK).build();
}
@POST
@Timed
@Path("accept")
@UnitOfWork
public Response accept(AcceptRequest req, @Context HttpServletRequest request) {
Invite inv = this.inviteDAO.findByCode(req.getInviteCode());
if(inv == null) throw new NotFoundException();
User oldUser = ud.findByUsername(req.getUsername());
if(oldUser != null) {
throw new ConflictException("That username is already taken");
}
User emailUser = ud.findByEmail(inv.getEmail());
if(emailUser != null) {
throw new ConflictException("That email already has an account. Consider using password recovery");
}
if(!validatePassword(req.getPassword())) {
throw new ConflictException("Password does not follow the complexity rules");
}
User u = new User();
u.setEmail(inv.getEmail());
u.setUsername(req.getUsername());
u.setPassword(req.getPassword());
u.setVerified(true);
u.setVerifyCode(inv.getInviteCode());
u.setVerifyCodeSentDate(inv.getInviteCodeSentDate());
Long userid = null;
try {
userid = ud.create(u);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e1) {
log.error("can't create the user because ",e1);
throw new WebApplicationException(e1);
}
// OK now make a User Project record also!!
UserProject up = new UserProject();
User u2 = this.ud.findById(userid.intValue());
Project p2 = this.projectDAO.findById(inv.getProject().getId());
up.setProject(p2);
up.setUser(u2);
up.setCanAdmin(inv.getCanAdmin());
up.setCanCategorize(inv.getCanCategorize());
up.setCanReport(inv.getCanReport());
up.setCanUpload(inv.getCanReport());
this.upDAO.save(up);
loadAssociatedAndSetSessionWith(request, u2);
loadStartingProject(request,u2);
//now delete the invite
this.inviteDAO.delete(inv);
String sur ="done";
return Response.ok(sur, MediaType.APPLICATION_JSON).build();
}
@POST
@Timed
@Path("signup")
@UnitOfWork
public Response signup(SignupRequest req) {
log.info("sign up started");
User oldUser = ud.findByUsername(req.getUsername());
if(oldUser != null) {
throw new ConflictException("That username is already taken");
}
User emailUser = ud.findByEmail(req.getEmail());
if(emailUser != null) {
throw new ConflictException("That email already has an account. Consider using password recovery");
}
if(!validatePassword(req.getPassword())) {
throw new ConflictException("Password does not follow the complexity rules");
}
log.info("no conflicts. ");
User u = new User();
u.setEmail(req.getEmail());
u.setUsername(req.getUsername());
u.setPassword(req.getPassword());
String verifyCode;
try {
verifyCode = makeVerifyCode();
} catch (NoSuchAlgorithmException e1) {
throw new ConflictException("unable to create a verification code");
}
log.info(" verify code created");
u.setVerifyCode(verifyCode);
try {
long userid = ud.create(u);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e1) {
log.error("can't create the user because ",e1);
throw new WebApplicationException(e1);
}
log.info(" user created");
DateTime verifySent = null;
try {
emailService.sendVerificationEmail(u);
verifySent = new DateTime();
} catch (MessagingException e) {
log.error("unable to send email because of ",e);
throw new ConflictException("unable to send an email to that address");
}
u.setVerifyCodeSentDate(verifySent);
// note that the update is automatic thanks to hibernate session magic!!
SignUpResponse sur = new SignUpResponse(true,"worked!");
return Response.ok(sur, MediaType.APPLICATION_JSON).build();
}
public static int indexOf(Pattern pattern, String s) {
Matcher matcher = pattern.matcher(s);
return matcher.find() ? matcher.start() : -1;
}
public static boolean validatePassword(String password) {
// must be 8 or longer characters
if(password.length() < 8) {
return false;
}
// must have a letter
int index = indexOf(Pattern.compile("[A-z]"), password);
if(index < 0) return false;
// must have a number
int index2 = indexOf(Pattern.compile("[0-9]"), password);
if(index2 < 0) return false;
return true;
}
@GET
@Timed
@Path("validtoken/{token}")
@UnitOfWork
public Response isValidToken(@PathParam("token") String token) {
if(tokendao.isTokenValid(token)) {
return Response.status(Response.Status.OK).build();
} else {
ResponseBuilder b = Response.status(Status.NOT_FOUND);
b.tag("invalid password reset token");
return b.build();
}
}
@POST
@Timed
@Path("lostpw")
@UnitOfWork
public Response lostpw(String resetemail) {
log.info("starting reset password on "+resetemail);
// now create and send via email a token which will return to a page on which a new PW can be chosen.
// that page should simple reset the password IF the token is valid and matches the email.
// token should live for a fixed amount of time. 10 minutes ?
User p = this.ud.findByEmail(resetemail);
if(p != null) {
String token = tokendao.createToken(p);
try {
emailService.sendPasswordResetEmail(p, token);
} catch (MessagingException e) {
}
}
// return OK regardless so that we don't "leak" data about membership.
return Response.status(Response.Status.OK).build();
}
@POST
@Timed
@Path("resetpw")
@UnitOfWork
public Response resetpw(ResetPWRequest request) {
log.info("starting reset password on "+request);
User u = tokendao.getUserForToken(request.getToken());
try {
if(u != null) {
if(!validatePassword(request.getPass())) {
throw new ConflictException("Password does not follow the complexity rules");
}
String pass = request.getPass();
String hashedPW = HibernateUserDAO.getSaltedHash(pass);
u.setHashedPassword(hashedPW);
this.emailService.sendUpdatedPassword(u);
return Response.status(Response.Status.OK).build();
}
} catch (NoSuchAlgorithmException e) {
log.error("unable to update password for "+u,e);
} catch (InvalidKeySpecException e) {
log.error("unable to update password for "+u,e);
} catch (MessagingException e) {
log.error("unable to update password for "+u,e);
}
ResponseBuilder b = Response.status(Status.NOT_FOUND);
b.tag("invalid password reset token");
return b.build();
}
@POST
@Timed
@Path("setpassword")
@UnitOfWork
public Response setpassword(@SessionUser User user, SetPWRequest request) {
log.info("starting reset password on "+request);
try {
if(user != null) {
String pass = request.getPass();
String hashedPW = HibernateUserDAO.getSaltedHash(pass);
user.setHashedPassword(hashedPW);
this.emailService.sendUpdatedPassword(user);
return Response.status(Response.Status.OK).build();
}
} catch (NoSuchAlgorithmException e) {
log.error("unable to set password for "+user,e);
} catch (InvalidKeySpecException e) {
log.error("unable to set password for "+user,e);
} catch (MessagingException e) {
log.error("unable to set password for "+user,e);
}
ResponseBuilder b = Response.status(Status.NOT_FOUND);
b.tag("unable to reset password");
return b.build();
}
final boolean autoLoginOnVerify = true;
@POST
@Timed
@Path("verify")
@UnitOfWork
public Response verify(@Context HttpServletRequest request,VerifyRequest req) {
String code = req.getVerifyCode();
User p = ud.findByVerifyCode(code);
if(p == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
p.setVerified(true);
try {
this.emailService.sendUsANotification(p);
} catch (MessagingException e) {
// do nothing here
}
if(autoLoginOnVerify) {
loadAssociatedAndSetSessionWith(request,p);
loadStartingProject(request,p);
}
VerifyResponse vr = new VerifyResponse(p);
return Response.ok(vr, MediaType.APPLICATION_JSON).build();
}
final static int CODE_LENGTH = 25;
public static String makeVerifyCode() throws NoSuchAlgorithmException {
int saltLen = 35;
String finalCode = null;
int len =0;
do {
byte[] randomBytes = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen);
String code = Base64.encodeBase64String(randomBytes);
String newCode = code.replaceAll("[^a-zA-Z0-9]" , "");
finalCode = newCode.substring(0, CODE_LENGTH);
len = finalCode.length();
} while(len != CODE_LENGTH);
return finalCode;
}
}