/* ================================================================== * MyNodesController.java - Nov 22, 2012 7:25:44 AM * * Copyright 2007-2012 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.central.reg.web; import static net.solarnetwork.web.domain.Response.response; import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import javax.servlet.http.HttpServletResponse; import org.joda.time.DateTime; import org.joda.time.ReadablePeriod; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.util.UriComponentsBuilder; import net.solarnetwork.central.RepeatableTaskException; import net.solarnetwork.central.mail.MailService; import net.solarnetwork.central.mail.support.BasicMailAddress; import net.solarnetwork.central.mail.support.ClasspathResourceMessageTemplateDataSource; import net.solarnetwork.central.security.AuthorizationException; import net.solarnetwork.central.security.SecurityUser; import net.solarnetwork.central.security.SecurityUtils; import net.solarnetwork.central.user.biz.NodeOwnershipBiz; import net.solarnetwork.central.user.biz.RegistrationBiz; import net.solarnetwork.central.user.biz.UserBiz; import net.solarnetwork.central.user.domain.NewNodeRequest; import net.solarnetwork.central.user.domain.User; import net.solarnetwork.central.user.domain.UserAlertStatus; import net.solarnetwork.central.user.domain.UserAlertType; import net.solarnetwork.central.user.domain.UserNode; import net.solarnetwork.central.user.domain.UserNodeCertificate; import net.solarnetwork.central.user.domain.UserNodeCertificateInstallationStatus; import net.solarnetwork.central.user.domain.UserNodeCertificateRenewal; import net.solarnetwork.central.user.domain.UserNodeConfirmation; import net.solarnetwork.central.user.domain.UserNodeTransfer; import net.solarnetwork.domain.NetworkAssociation; import net.solarnetwork.domain.NetworkCertificate; import net.solarnetwork.support.CertificateException; import net.solarnetwork.support.CertificateService; import net.solarnetwork.web.domain.Response; /** * Controller for "my nodes". * * @author matt * @version 1.4 */ @Controller @RequestMapping("/sec/my-nodes") public class MyNodesController extends ControllerSupport { private final UserBiz userBiz; private final RegistrationBiz registrationBiz; private final NodeOwnershipBiz nodeOwnershipBiz; private final CertificateService certificateService; @Autowired(required = false) private MailService mailService; @Autowired private MessageSource messageSource; /** * Constructor. * * @param userBiz * The {@link UserBiz} to use. * @param registrationBiz * The {@link RegistrationBiz} to use. * @param nodeOwnershipBiz * the {@link NodeOwnershipBiz} to use. * @param certificateService * The {@link CertificateService} to use. */ @Autowired public MyNodesController(UserBiz userBiz, RegistrationBiz registrationBiz, NodeOwnershipBiz nodeOwnershipBiz, CertificateService certificateService) { super(); this.userBiz = userBiz; this.registrationBiz = registrationBiz; this.certificateService = certificateService; this.nodeOwnershipBiz = nodeOwnershipBiz; } /** * Set a {@link MailService} to use. * * @param mailService * The service to use. */ public void setMailService(MailService mailService) { this.mailService = mailService; } /** * The {@link MessageSource} to use in conjunction with * {@link #setMailService(MailService)}. * * @param messageSource * A message source to use. */ public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } @ModelAttribute("nodeDataAlertTypes") public List<UserAlertType> nodeDataAlertTypes() { // now now, only one alert type! return Collections.singletonList(UserAlertType.NodeStaleData); } @ModelAttribute("alertStatuses") public UserAlertStatus[] alertStatuses() { return UserAlertStatus.values(); } /** * View a "home" page for the "my nodes" section. * * @return model and view */ @RequestMapping(value = "", method = RequestMethod.GET) public ModelAndView viewMyNodes() { final SecurityUser actor = SecurityUtils.getCurrentUser(); List<UserNode> nodes = userBiz.getUserNodes(SecurityUtils.getCurrentUser().getUserId()); // move any nodes with pending transfer into own list List<UserNode> pendingTransferNodes = new ArrayList<UserNode>(nodes == null ? 0 : nodes.size()); if ( nodes != null ) { for ( Iterator<UserNode> itr = nodes.iterator(); itr.hasNext(); ) { UserNode node = itr.next(); if ( node.getTransfer() != null ) { itr.remove(); pendingTransferNodes.add(node); } } } List<UserNodeConfirmation> pendingConfirmationList = userBiz .getPendingUserNodeConfirmations(actor.getUserId()); List<UserNodeTransfer> pendingNodeOwnershipRequests = nodeOwnershipBiz .pendingNodeOwnershipTransfersForEmail(actor.getEmail()); ModelAndView mv = new ModelAndView("my-nodes/my-nodes"); mv.addObject("userNodesList", nodes); mv.addObject("pendingUserNodeConfirmationsList", pendingConfirmationList); mv.addObject("pendingUserNodeTransferList", pendingTransferNodes); mv.addObject("pendingNodeOwnershipRequests", pendingNodeOwnershipRequests); return mv; } /** * Get a list of all archived nodes. * * @return All archived nodes. * @since 1.4 */ @RequestMapping(value = "/archived", method = RequestMethod.GET) @ResponseBody public Response<List<UserNode>> getArchivedNodes() { final SecurityUser actor = SecurityUtils.getCurrentUser(); List<UserNode> nodes = userBiz.getArchivedUserNodes(actor.getUserId()); return Response.response(nodes); } /** * Update the archived status of a set of nodes. * * @param nodeIds * The node IDs to update the archived status of. * @param archived * {@code true} to archive, {@code false} to un-archive * @return A success response. * @since 1.4 */ @RequestMapping(value = "/archived", method = RequestMethod.POST) @ResponseBody public Response<Object> updateArchivedStatus(@RequestParam("nodeIds") Long[] nodeIds, @RequestParam("archived") boolean archived) { final SecurityUser actor = SecurityUtils.getCurrentUser(); userBiz.updateUserNodeArchivedStatus(actor.getUserId(), nodeIds, archived); return Response.response(null); } /** * Generate a new node confirmation code. * * @param userId * the optional user ID to generate the code for; defaults to the * acting user * @param securityPhrase * a security phrase to associate with the invitation * @param timeZoneName * the time zone to associate the node with * @param country * the country to associate the node with * @return model and view */ @RequestMapping("/new") public ModelAndView newNodeAssociation(@RequestParam(value = "userId", required = false) Long userId, @RequestParam("phrase") String securityPhrase, @RequestParam("timeZone") String timeZoneName, @RequestParam("country") String countryCode) { if ( userId == null ) { userId = SecurityUtils.getCurrentUser().getUserId(); } final TimeZone timeZone = TimeZone.getTimeZone(timeZoneName); String lang = "en"; for ( Locale locale : Locale.getAvailableLocales() ) { if ( locale.getCountry().equals(countryCode) ) { lang = locale.getLanguage(); } } final Locale locale = new Locale(lang, countryCode); final NetworkAssociation details = registrationBiz .createNodeAssociation(new NewNodeRequest(userId, securityPhrase, timeZone, locale)); return new ModelAndView("my-nodes/invitation", "details", details); } @RequestMapping("/tzpicker.html") public String tzpicker() { return "tzpicker-500"; } @RequestMapping("/invitation") public ModelAndView viewConfirmation(@RequestParam(value = "id") Long userNodeConfirmationId) { NetworkAssociation details = registrationBiz.getNodeAssociation(userNodeConfirmationId); return new ModelAndView("my-nodes/invitation", "details", details); } @RequestMapping("/cancelInvitation") public String cancelConfirmation(@RequestParam(value = "id") Long userNodeConfirmationId) { registrationBiz.cancelNodeAssociation(userNodeConfirmationId); return "redirect:/u/sec/my-nodes"; } /** * Get a certificate, either as a {@link UserNodeCertificate} object or the * PEM encoded value file attachment. * * @param certId * the ID of the certificate to get * @param download * if TRUE, then download the certificate as a PEM file * @return the response data */ @RequestMapping(value = "/cert/{nodeId}", method = RequestMethod.GET) @ResponseBody public ResponseEntity<byte[]> viewCert(@PathVariable("nodeId") Long nodeId) { SecurityUser actor = SecurityUtils.getCurrentUser(); UserNodeCertificate cert = userBiz.getUserNodeCertificate(actor.getUserId(), nodeId); if ( cert == null ) { throw new AuthorizationException(AuthorizationException.Reason.ACCESS_DENIED, nodeId); } final byte[] data = cert.getKeystoreData(); HttpHeaders headers = new HttpHeaders(); headers.setContentLength(data.length); headers.setContentType(MediaType.parseMediaType("application/x-pkcs12")); headers.setLastModified(System.currentTimeMillis()); headers.setCacheControl("no-cache"); headers.set("Content-Disposition", "attachment; filename=solarnode-" + cert.getNode().getId() + ".p12"); return new ResponseEntity<byte[]>(data, headers, HttpStatus.OK); } /** * Get a certificate as a {@link UserNodeCertificate} object or the PEM * encoded value file attachment. * * @param certId * the ID of the certificate to get * @param password * the password to decrypt the certificate store with * @return the response data * @since 1.3 */ @RequestMapping(value = "/cert/{nodeId}", method = RequestMethod.POST) @ResponseBody public UserNodeCertificate viewCert(@PathVariable("nodeId") Long nodeId, @RequestParam(value = "password") String password) { SecurityUser actor = SecurityUtils.getCurrentUser(); UserNodeCertificate cert = userBiz.getUserNodeCertificate(actor.getUserId(), nodeId); if ( cert == null ) { throw new AuthorizationException(AuthorizationException.Reason.ACCESS_DENIED, nodeId); } final byte[] data = cert.getKeystoreData(); // see if a renewal is pending UserNodeCertificateInstallationStatus installationStatus = null; if ( cert.getRequestId() != null ) { UserNode userNode = new UserNode(cert.getUser(), cert.getNode()); UserNodeCertificateRenewal renewal = registrationBiz .getPendingNodeCertificateRenewal(userNode, cert.getRequestId()); if ( renewal != null ) { installationStatus = renewal.getInstallationStatus(); } } String pkcs7 = ""; X509Certificate nodeCert = null; if ( data != null ) { KeyStore keystore = cert.getKeyStore(password); X509Certificate[] chain = cert.getNodeCertificateChain(keystore); if ( chain != null && chain.length > 0 ) { nodeCert = chain[0]; } pkcs7 = certificateService.generatePKCS7CertificateChainString(chain); } return new UserNodeCertificateDecoded(cert, installationStatus, nodeCert, pkcs7, registrationBiz.getNodeCertificateRenewalPeriod()); } /** * AuthorizationException handler. * * <p> * Logs a WARN log and returns HTTP 403 (Forbidden). * </p> * * @param e * the exception * @param res * the servlet response */ @ExceptionHandler(CertificateException.class) public void handleCertificateException(CertificateException e, HttpServletResponse res) { if ( log.isWarnEnabled() ) { log.warn("Certificate exception: " + e.getMessage()); } res.setStatus(HttpServletResponse.SC_FORBIDDEN); } @RequestMapping(value = "/cert/renew/{nodeId}", method = RequestMethod.POST) @ResponseBody public UserNodeCertificate renewCert(@PathVariable("nodeId") final Long nodeId, @RequestParam("password") final String password) { SecurityUser actor = SecurityUtils.getCurrentUser(); UserNode userNode = userBiz.getUserNode(actor.getUserId(), nodeId); if ( userNode == null ) { throw new AuthorizationException(AuthorizationException.Reason.ACCESS_DENIED, nodeId); } NetworkCertificate renewed = registrationBiz.renewNodeCertificate(userNode, password); if ( renewed != null && renewed.getNetworkCertificate() != null ) { return viewCert(nodeId, password); } throw new RepeatableTaskException("Certificate renewal processing"); } public static class UserNodeCertificateDecoded extends UserNodeCertificate { private static final long serialVersionUID = -2314002517991208690L; private final UserNodeCertificateInstallationStatus installationStatus; private final String pemValue; private final X509Certificate nodeCert; private final DateTime renewAfter; private UserNodeCertificateDecoded(UserNodeCertificate cert, UserNodeCertificateInstallationStatus installationStatus, X509Certificate nodeCert, String pkcs7, ReadablePeriod renewPeriod) { super(); setCreated(cert.getCreated()); setId(cert.getId()); setNodeId(cert.getNodeId()); setRequestId(cert.getRequestId()); setUserId(cert.getUserId()); this.installationStatus = installationStatus; this.pemValue = pkcs7; this.nodeCert = nodeCert; if ( nodeCert != null ) { if ( renewPeriod != null ) { this.renewAfter = new DateTime(nodeCert.getNotAfter()).minus(renewPeriod); } else { this.renewAfter = null; } } else { this.renewAfter = null; } } public String getPemValue() { return pemValue; } /** * Get a hexidecimal string value of the certificate serial number. * * @return The certificate serial number. */ public String getCertificateSerialNumber() { return (nodeCert != null ? "0x" + nodeCert.getSerialNumber().toString(16) : null); } /** * Get the date the certificate is valid from. * * @return The valid from date. */ public DateTime getCertificateValidFromDate() { return (nodeCert != null ? new DateTime(nodeCert.getNotBefore()) : null); } /** * Get the date the certificate is valid until. * * @return The valid until date. */ public DateTime getCertificateValidUntilDate() { return (nodeCert != null ? new DateTime(nodeCert.getNotAfter()) : null); } /** * Get the certificate subject DN. * * @return The certificate subject DN. */ public String getCertificateSubjectDN() { return (nodeCert != null ? nodeCert.getSubjectDN().getName() : null); } /** * Get the certificate issuer DN. * * @return The certificate issuer DN. */ public String getCertificateIssuerDN() { return (nodeCert != null ? nodeCert.getIssuerDN().getName() : null); } /** * Get a date after which the certificate may be renewed. * * @return A renewal minimum date. */ public DateTime getCertificateRenewAfterDate() { return renewAfter; } /** * Get the status of the installation process, if available. * * @return The installation status, or <em>null</em>. */ public UserNodeCertificateInstallationStatus getInstallationStatus() { return installationStatus; } } @RequestMapping(value = "/editNode", method = RequestMethod.GET) public String editNodeView(@RequestParam("userId") Long userId, @RequestParam("nodeId") Long nodeId, Model model) { model.addAttribute("userNode", userBiz.getUserNode(userId, nodeId)); return "my-nodes/edit-node"; } @ResponseBody @RequestMapping(value = "/node", method = RequestMethod.GET) public Response<UserNode> getUserNode(@RequestParam("userId") Long userId, @RequestParam("nodeId") Long nodeId) { return response(userBiz.getUserNode(userId, nodeId)); } @ResponseBody @RequestMapping(value = "/updateNode", method = RequestMethod.POST) public UserNode editNodeSave(UserNode userNode, Errors userNodeErrors, Model model) { return userBiz.saveUserNode(userNode); } /** * Request an ownership transfer of a node to another SolarNetwork account. * * @param userId * The user ID of the current node owner. * @param nodeId * The ID of the node to transfer ownership of. * @param email * The recipient of the node ownership request. * @param locale * The request locale to use in the generated email content. * @param uriBuilder * A URI builder to assist in the generated email content. * @return A {@code TRUE} value on success. */ @ResponseBody @RequestMapping(value = "/requestNodeTransfer", method = RequestMethod.POST) public Response<Boolean> requestNodeOwnershipTransfer(@RequestParam("userId") Long userId, @RequestParam("nodeId") Long nodeId, @RequestParam("recipient") String email, Locale locale, UriComponentsBuilder uriBuilder) { nodeOwnershipBiz.requestNodeOwnershipTransfer(userId, nodeId, email); if ( mailService != null ) { try { User actor = userBiz.getUser(SecurityUtils.getCurrentActorUserId()); uriBuilder.pathSegment("sec", "my-nodes"); Map<String, Object> mailModel = new HashMap<String, Object>(2); mailModel.put("actor", actor); mailModel.put("recipient", email); mailModel.put("nodeId", nodeId); mailModel.put("url", uriBuilder.build().toUriString()); mailService.sendMail(new BasicMailAddress(null, email), new ClasspathResourceMessageTemplateDataSource(locale, messageSource.getMessage("my-nodes.transferOwnership.mail.subject", null, locale), "/net/solarnetwork/central/reg/web/transfer-ownership.txt", mailModel)); } catch ( RuntimeException e ) { // ignore this other than log log.warn("Error sending ownership transfer mail message to {}: {}", email, e.getMessage(), e); } } return response(Boolean.TRUE); } @ResponseBody @RequestMapping(value = "/cancelNodeTransferRequest", method = RequestMethod.POST) public Response<Boolean> cancelNodeOwnershipTransfer(@RequestParam("userId") Long userId, @RequestParam("nodeId") Long nodeId, Locale locale) { UserNodeTransfer xfer = nodeOwnershipBiz.getNodeOwnershipTransfer(userId, nodeId); if ( xfer != null ) { nodeOwnershipBiz.cancelNodeOwnershipTransfer(userId, nodeId); if ( mailService != null ) { // notify the recipient about the cancellation try { User actor = userBiz.getUser(SecurityUtils.getCurrentActorUserId()); Map<String, Object> mailModel = new HashMap<String, Object>(2); mailModel.put("actor", actor); mailModel.put("transfer", xfer); mailService.sendMail(new BasicMailAddress(null, xfer.getEmail()), new ClasspathResourceMessageTemplateDataSource(locale, messageSource.getMessage( "my-nodes.transferOwnership.mail.subject.cancelled", null, locale), "/net/solarnetwork/central/reg/web/transfer-ownership-cancelled.txt", mailModel)); } catch ( RuntimeException e ) { // ignore this other than log log.warn("Error sending ownership transfer mail message to {}: {}", xfer.getEmail(), e.getMessage(), e); } } } return response(Boolean.TRUE); } @ResponseBody @RequestMapping(value = "/confirmNodeTransferRequest", method = RequestMethod.POST) public Response<Boolean> confirmNodeOwnershipTransfer(@RequestParam("userId") Long userId, @RequestParam("nodeId") Long nodeId, @RequestParam("accept") boolean accept, Locale locale) { UserNodeTransfer xfer = nodeOwnershipBiz.confirmNodeOwnershipTransfer(userId, nodeId, accept); if ( xfer != null ) { if ( mailService != null ) { // notify the recipient about the cancellation try { User actor = userBiz.getUser(SecurityUtils.getCurrentActorUserId()); Map<String, Object> mailModel = new HashMap<String, Object>(2); mailModel.put("actor", actor); mailModel.put("transfer", xfer); mailService .sendMail(new BasicMailAddress(null, xfer.getUser().getEmail()), new ClasspathResourceMessageTemplateDataSource(locale, messageSource.getMessage( ("my-nodes.transferOwnership.mail.subject." + (accept ? "accepted" : "declined")), null, locale), ("/net/solarnetwork/central/reg/web/transfer-ownership-" + (accept ? "accepted" : "declined") + ".txt"), mailModel)); } catch ( RuntimeException e ) { // ignore this other than log log.warn("Error sending ownership transfer mail message to {}: {}", xfer.getEmail(), e.getMessage(), e); } } } return response(Boolean.TRUE); } }