001package org.deepamehta.plugins.signup; 002 003import com.sun.jersey.api.view.Viewable; 004import de.deepamehta.core.Association; 005import de.deepamehta.core.ChildTopics; 006import de.deepamehta.core.RelatedTopic; 007import de.deepamehta.core.Topic; 008import de.deepamehta.core.model.*; 009import de.deepamehta.core.service.DeepaMehtaEvent; 010import de.deepamehta.core.service.DeepaMehtaService; 011import de.deepamehta.core.service.EventListener; 012import de.deepamehta.core.service.Inject; 013import de.deepamehta.core.service.ResultList; 014import de.deepamehta.core.service.accesscontrol.AccessControl; 015import de.deepamehta.core.service.accesscontrol.Credentials; 016import de.deepamehta.core.service.event.PostUpdateTopicListener; 017import de.deepamehta.core.storage.spi.DeepaMehtaTransaction; 018import de.deepamehta.plugins.accesscontrol.AccessControlService; 019import de.deepamehta.plugins.webactivator.WebActivatorPlugin; 020import de.deepamehta.plugins.workspaces.WorkspacesService; 021import java.net.MalformedURLException; 022import java.net.URL; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Date; 026import java.util.HashMap; 027import java.util.Iterator; 028import java.util.UUID; 029import java.util.concurrent.Callable; 030import java.util.logging.Level; 031import java.util.logging.Logger; 032import javax.mail.internet.InternetAddress; 033import javax.ws.rs.*; 034import javax.ws.rs.core.Context; 035import javax.ws.rs.core.MediaType; 036import javax.ws.rs.core.UriInfo; 037import org.apache.commons.mail.HtmlEmail; 038import org.codehaus.jettison.json.JSONException; 039import org.codehaus.jettison.json.JSONObject; 040import org.deepamehta.plugins.signup.service.SignupPluginService; 041 042 043/** 044 * Routes registerd by this plugin are: 045 * 046 * / => login-form 047 * /sign-up => signup-form 048 * /ok => info-page after account creation 049 * 050 * @name dm4-sign-up 051 * @website https://github.com/mukil/dm4-sign-up 052 * @version 1.1-SNAPSHOT 053 * @author <a href="mailto:malte@mikromedia.de">Malte Reissig</a>; 054 */ 055 056@Path("/") 057public class SignupPlugin extends WebActivatorPlugin implements SignupPluginService, 058 PostUpdateTopicListener { 059 060 private static Logger log = Logger.getLogger(SignupPlugin.class.getName()); 061 062 // --- DeepaMehta 4 related type URIs 063 064 public static final String MAILBOX_TYPE_URI = "dm4.contacts.email_address"; 065 public final static String WS_DM_DEFAULT_URI = "de.workspaces.deepamehta"; 066 067 // --- Sign-up related type URIs (Configuration, Template Data) 068 069 private final String SIGN_UP_PLUGIN_TOPIC_URI = "org.deepamehta.sign-up"; 070 private final String SIGN_UP_CONFIG_TYPE_URI = "org.deepamehta.signup.configuration"; 071 private final String CONFIG_PROJECT_TITLE = "org.deepamehta.signup.config_project_title"; 072 private final String CONFIG_WEBAPP_TITLE = "org.deepamehta.signup.config_webapp_title"; 073 private final String CONFIG_LOGO_PATH = "org.deepamehta.signup.config_webapp_logo_path"; 074 private final String CONFIG_CSS_PATH = "org.deepamehta.signup.config_custom_css_path"; 075 private final String CONFIG_READ_MORE_URL = "org.deepamehta.signup.config_read_more_url"; 076 private final String CONFIG_PAGES_FOOTER = "org.deepamehta.signup.config_pages_footer"; 077 private final String CONFIG_TOS_LABEL = "org.deepamehta.signup.config_tos_label"; 078 private final String CONFIG_TOS_DETAILS = "org.deepamehta.signup.config_tos_detail"; 079 private final String CONFIG_PD_LABEL = "org.deepamehta.signup.config_pd_label"; 080 private final String CONFIG_PD_DETAILS = "org.deepamehta.signup.config_pd_detail"; 081 082 private Topic currentModuleConfiguration = null; 083 084 @Inject 085 private AccessControlService acService; 086 087 @Inject /*** Used in migration */ 088 private WorkspacesService wsService; 089 090 @Context UriInfo uri; 091 092 HashMap<String, JSONObject> token = new HashMap<String, JSONObject>(); 093 094 095 @Override 096 public void init() { 097 initTemplateEngine(); 098 reloadConfiguration(); 099 } 100 101 static DeepaMehtaEvent USER_ACCOUNT_CREATE_LISTENER = new DeepaMehtaEvent(UserAccountCreateListener.class) { 102 @Override 103 public void deliver(EventListener listener, Object... params) { 104 ((UserAccountCreateListener) listener).userAccountCreated((Topic) params[0]); 105 } 106 }; 107 108 /** Plugin Service Implementation */ 109 @GET 110 @Path("/sign-up/check/{username}") 111 @Produces(MediaType.APPLICATION_JSON) 112 public String getUsernameAvailability(@PathParam("username") String username) { 113 JSONObject response = new JSONObject(); 114 try { 115 response.put("isAvailable", true); 116 if (isUsernameTaken(username)) response.put("isAvailable", false); 117 return response.toString(); 118 } catch (Exception e) { 119 throw new RuntimeException(e); 120 } 121 } 122 123 @GET 124 @Path("/sign-up/check/mailbox/{email}") 125 @Produces(MediaType.APPLICATION_JSON) 126 public String getMailboxAvailability(@PathParam("email") String email) { 127 JSONObject response = new JSONObject(); 128 try { 129 response.put("isAvailable", true); 130 if (isMailboxTaken(email)) response.put("isAvailable", false); 131 return response.toString(); 132 } catch (Exception e) { 133 throw new RuntimeException(e); 134 } 135 } 136 137 private String createSimpleUserAccount(@PathParam("username") String username, @PathParam("pass-one") String password, 138 @PathParam("mailbox") String mailbox) { 139 DeepaMehtaTransaction tx = dms.beginTx(); 140 try { 141 if (isUsernameTaken(username)) throw new RuntimeException("Username is unavailable!"); 142 Credentials creds = new Credentials(new JSONObject() 143 .put("username", username) 144 .put("password", password)); 145 // 1) Create new user (in which workspace), just within the private one, no? 146 final Topic usernameTopic = acService.createUserAccount(creds); 147 final String eMailAddressValue = mailbox; 148 // 2) create and associate e-mail address topic 149 dms.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Topic>() { 150 @Override 151 public Topic call() { 152 Topic eMailAddress = dms.createTopic(new TopicModel(MAILBOX_TYPE_URI, new SimpleValue(eMailAddressValue))); 153 // 3) fire custom event ### this is useless since fired by "anonymous" (this request scope) 154 // dms.fireEvent(USER_ACCOUNT_CREATE_LISTENER, user); 155 AccessControl acCore = dms.getAccessControl(); 156 // 4) assign new e-mail address topic to admins "Private workspace" 157 Topic adminWorkspace = dms.getAccessControl().getPrivateWorkspace("admin"); 158 acCore.assignToWorkspace(eMailAddress, adminWorkspace.getId()); 159 // 5) associate email address to "username" topic too 160 Association assoc = dms.createAssociation(new AssociationModel("dm4.core.association", 161 new TopicRoleModel(eMailAddress.getId(), "dm4.core.child"), 162 new TopicRoleModel(usernameTopic.getId(), "dm4.core.parent"))); 163 // 6) assign that association also to admins "Private Workspace" 164 acCore.assignToWorkspace(assoc, adminWorkspace.getId()); 165 return eMailAddress; 166 } 167 }); 168 log.info("Created new user account for user \"" + username + "\" and " + eMailAddressValue); 169 // 6) Inform administrations about successfull account creation 170 sendNotificationMail(username, mailbox.trim()); 171 tx.success(); 172 return username; 173 } catch (Exception e) { 174 throw new RuntimeException("Creating simple user account FAILED!", e); 175 } finally { 176 tx.finish(); 177 } 178 } 179 180 @GET 181 @Path("/sign-up/send/{username}/{pass-one}/{mailbox}") 182 public String createUserValidationToken(@PathParam("username") String username, 183 @PathParam("pass-one") String password, @PathParam("mailbox") String mailbox) { 184 // 185 String response = null; 186 try { 187 String key = UUID.randomUUID().toString(); 188 long valid = new Date().getTime() + 3600000; // Token is valid fo 60 min 189 JSONObject value = new JSONObject(); 190 value.put("username", username.trim()); 191 value.put("mailbox", mailbox.trim()); 192 value.put("password", password); 193 value.put("expiration", valid); 194 token.put(key, value); 195 log.info("Set up key " +key+ " for "+mailbox+" sending confirmation mail valid till " 196 + new Date(valid).toString()); 197 sendConfirmationMail(key, username, mailbox.trim()); 198 } catch (JSONException ex) { 199 Logger.getLogger(SignupPlugin.class.getName()).log(Level.SEVERE, null, ex); 200 throw new RuntimeException(ex); 201 } 202 return response; 203 } 204 205 @GET 206 @Path("/sign-up/confirm/{token}") 207 public Viewable handleTokenRequest(@PathParam("token") String key) { 208 String username; 209 if (token.isEmpty()) throw new RuntimeException("Sorry, we lost all the tokens!"); 210 try { 211 JSONObject input = token.get(key); 212 token.remove(key); 213 username = input.getString("username"); 214 if (input.getLong("expiration") > new Date().getTime()) { 215 log.info("Trying to create user account for " + input.getString("mailbox")); 216 createSimpleUserAccount(username, input.getString("password"), input.getString("mailbox")); 217 } else { 218 viewData("username", null); 219 viewData("message", "Timeout"); 220 return view("failure"); 221 } 222 } catch (JSONException ex) { 223 Logger.getLogger(SignupPlugin.class.getName()).log(Level.SEVERE, null, ex); 224 viewData("message", "An error occured processing your request"); 225 log.severe("Account creation failed due to " + ex.getMessage() + " caused by " + ex.getCause().toString()); 226 return view("failure"); 227 } 228 log.info("Account confirmed & succesfully created, username: " + username); 229 viewData("username", username); 230 viewData("message", "User account created successfully"); 231 return getAccountCreationOKView(); 232 } 233 234 235 236 /** --- Private Helpers --- */ 237 238 /** 239 * Loads the next sign-up configuration topic for this plugin. 240 * 241 * @see init() 242 * @see postUpdateTopic() 243 */ 244 private Topic reloadConfiguration() { 245 log.info("Sign-up: Reloading sign-up plugin configuration."); 246 currentModuleConfiguration = getCurrentSignupConfiguration(); 247 currentModuleConfiguration.loadChildTopics(); 248 log.info("Sign-up: Loaded sign-up configuration => \"" + currentModuleConfiguration.getUri() 249 + "\", \"" + currentModuleConfiguration.getSimpleValue() + "\""); 250 return currentModuleConfiguration; 251 } 252 253 private void sendConfirmationMail(String key, String username, String mailbox) { 254 try { 255 String webAppTitle = currentModuleConfiguration.getChildTopics() 256 .getString("org.deepamehta.signup.config_webapp_title"); 257 URL url = uri.getBaseUri().toURL(); 258 log.info("The confirmation mails token request URL should be:" 259 + "\n" + url + "sign-up/confirm/" + key); 260 sendSystemMail("Your account on " + webAppTitle, 261 "Hi "+username+",\n\nyou can complete the account registration process for " + webAppTitle 262 + " through visiting the following webpage:\n" + url + "sign-up/confirm/" + key 263 + "\n\nCheers!\n\n", mailbox); 264 } catch (MalformedURLException ex) { 265 throw new RuntimeException(ex); 266 } 267 } 268 269 private void sendNotificationMail(String username, String mailbox) { 270 String webAppTitle = currentModuleConfiguration.getChildTopics().getString("org.deepamehta.signup.config_webapp_title"); 271 sendSystemMail("Account registration on " + webAppTitle, 272 "\nA user has registered.\n\nUsername: " + username + "\nEmail: "+mailbox+"\n\n", null); 273 } 274 275 private void sendSystemMail(String subject, String message, String recipient) { 276 // Fix: Classloader issue we have in OSGi since using Pax web 277 Thread.currentThread().setContextClassLoader(SignupPlugin.class.getClassLoader()); 278 log.info("BeforeSend: Set classloader to " + Thread.currentThread().getContextClassLoader().toString()); 279 HtmlEmail email = new HtmlEmail(); 280 email.setDebug(true); // => System.out.println(SMTP communication); 281 email.setHostName("localhost"); // ### use getBaseUri() from HTTP Context? 282 try { 283 // ..) Set Senders of Mail 284 String projectName = currentModuleConfiguration.getChildTopics().getString("org.deepamehta.signup.config_project_title"); 285 String sender = currentModuleConfiguration.getChildTopics().getString("org.deepamehta.signup.config_from_mailbox"); 286 email.setFrom(sender.trim(), projectName.trim()); 287 // ..) Set Subject of Mail 288 email.setSubject(subject); 289 // ..) Set Message Body 290 email.setTextMsg(message); 291 // ..) Set recipient of notification mail 292 String recipientValue; 293 if (recipient != null) { 294 recipientValue = recipient.trim(); 295 } else { 296 recipientValue = currentModuleConfiguration.getChildTopics() 297 .getString("org.deepamehta.signup.config_admin_mailbox").trim(); 298 } 299 log.info("Loaded current configuration topic, sending notification mail to " + recipientValue); 300 Collection<InternetAddress> recipients = new ArrayList<InternetAddress>(); 301 recipients.add(new InternetAddress(recipientValue)); 302 email.setTo(recipients); 303 email.send(); 304 log.info("Mail was SUCCESSFULLY sent to " + email.getToAddresses() + " mail addresses"); 305 } catch (Exception ex) { 306 throw new RuntimeException("Sending notification mail FAILED", ex); 307 } finally { 308 // Fix: Classloader issue we have in OSGi since using Pax web 309 Thread.currentThread().setContextClassLoader(DeepaMehtaService.class.getClassLoader()); 310 log.info("AfterSend: Set Classloader back " + Thread.currentThread().getContextClassLoader().toString()); 311 } 312 } 313 314 private boolean isUsernameTaken(String username) { 315 String value = username.trim(); 316 Topic userNameTopic = acService.getUsernameTopic(value); 317 return (userNameTopic != null); 318 } 319 320 private boolean isMailboxTaken(String email) { 321 String value = email.toLowerCase().trim(); 322 return dms.getAccessControl().emailAddressExists(value); 323 } 324 325 /** 326 * The sign-up configuration object is loaded once when this bundle/plugin is 327 * initialized by the framework and as soon as one configuration was edited. 328 * 329 * @see reloadConfiguration() 330 */ 331 private Topic getCurrentSignupConfiguration() { 332 Topic pluginTopic = dms.getTopic("uri", new SimpleValue(SIGN_UP_PLUGIN_TOPIC_URI)); 333 return pluginTopic.getRelatedTopic("dm4.core.association", "dm4.core.default", "dm4.core.default", 334 SIGN_UP_CONFIG_TYPE_URI); 335 } 336 337 338 339 /** --- Sign-up Routes --- */ 340 341 @GET 342 @Produces(MediaType.TEXT_HTML) 343 public Viewable getLoginFormView() { 344 // fixme: use acl service to check if a session already exists and if so, redirect to dm-webclient directly 345 prepareSignupPage(); 346 return view("login"); 347 } 348 349 @GET 350 @Path("/sign-up") 351 @Produces(MediaType.TEXT_HTML) 352 public Viewable getSignupFormView() { 353 // fixme: use acl service to check if a session already exists and if so, redirect to dm-webclient directly 354 prepareSignupPage(); 355 return view("sign-up"); 356 } 357 358 @GET 359 @Path("/ok") 360 @Produces(MediaType.TEXT_HTML) 361 public Viewable getAccountCreationOKView() { 362 prepareSignupPage(); 363 return view("ok"); 364 } 365 366 @GET 367 @Path("/error") 368 @Produces(MediaType.TEXT_HTML) 369 public Viewable getFailureView() { 370 prepareSignupPage(); 371 return view("failure"); 372 } 373 374 @GET 375 @Path("/token-info") 376 @Produces(MediaType.TEXT_HTML) 377 public Viewable getConfirmationInfoView() { 378 prepareSignupPage(); 379 return view("confirmation"); 380 } 381 382 383 384 private void prepareSignupPage() { 385 if (currentModuleConfiguration != null) { 386 log.info("Preparing views according to current module configuration."); 387 ChildTopics configuration = currentModuleConfiguration.getChildTopics(); 388 viewData("title", configuration.getTopic(CONFIG_WEBAPP_TITLE).getSimpleValue().toString()); 389 viewData("logo_path", configuration.getTopic(CONFIG_LOGO_PATH).getSimpleValue().toString()); 390 viewData("css_path", configuration.getTopic(CONFIG_CSS_PATH).getSimpleValue().toString()); 391 viewData("project_name", configuration.getTopic(CONFIG_PROJECT_TITLE).getSimpleValue().toString()); 392 viewData("read_more_url", configuration.getTopic(CONFIG_READ_MORE_URL).getSimpleValue().toString()); 393 viewData("tos_label", configuration.getTopic(CONFIG_TOS_LABEL).getSimpleValue().toString()); 394 viewData("tos_details", configuration.getTopic(CONFIG_TOS_DETAILS).getSimpleValue().toString()); 395 viewData("pd_label", configuration.getTopic(CONFIG_PD_LABEL).getSimpleValue().toString()); 396 viewData("pd_details", configuration.getTopic(CONFIG_PD_DETAILS).getSimpleValue().toString()); 397 viewData("footer", configuration.getTopic(CONFIG_PAGES_FOOTER).getSimpleValue().toString()); 398 } else { 399 log.warning("Could not load module configuration!"); 400 } 401 402 } 403 404 public void postUpdateTopic(Topic topic, TopicModel tm, TopicModel tm1) { 405 if (topic.getTypeUri().equals(SIGN_UP_CONFIG_TYPE_URI)) { 406 reloadConfiguration(); 407 } 408 } 409 410}