001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2022 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.webconlet.examples.login; 020 021import freemarker.core.ParseException; 022import freemarker.template.MalformedTemplateNameException; 023import freemarker.template.Template; 024import freemarker.template.TemplateException; 025import freemarker.template.TemplateNotFoundException; 026import java.beans.ConstructorProperties; 027import java.io.IOException; 028import java.io.StringWriter; 029import java.util.HashSet; 030import java.util.Map; 031import java.util.Optional; 032import java.util.Set; 033import java.util.concurrent.Future; 034import javax.security.auth.Subject; 035import org.jgrapes.core.Channel; 036import org.jgrapes.core.Components; 037import org.jgrapes.core.Event; 038import org.jgrapes.core.Manager; 039import org.jgrapes.core.annotation.Handler; 040import org.jgrapes.http.events.DiscardSession; 041import org.jgrapes.io.events.Close; 042import org.jgrapes.webconsole.base.Conlet.RenderMode; 043import org.jgrapes.webconsole.base.ConletBaseModel; 044import org.jgrapes.webconsole.base.ConsoleConnection; 045import org.jgrapes.webconsole.base.ConsoleUser; 046import org.jgrapes.webconsole.base.WebConsoleUtils; 047import org.jgrapes.webconsole.base.events.AddConletRequest; 048import org.jgrapes.webconsole.base.events.AddConletType; 049import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; 050import org.jgrapes.webconsole.base.events.CloseModalDialog; 051import org.jgrapes.webconsole.base.events.ConsolePrepared; 052import org.jgrapes.webconsole.base.events.ConsoleReady; 053import org.jgrapes.webconsole.base.events.NotifyConletModel; 054import org.jgrapes.webconsole.base.events.NotifyConletView; 055import org.jgrapes.webconsole.base.events.OpenModalDialog; 056import org.jgrapes.webconsole.base.events.RenderConlet; 057import org.jgrapes.webconsole.base.events.RenderConletRequestBase; 058import org.jgrapes.webconsole.base.events.SetLocale; 059import org.jgrapes.webconsole.base.events.SimpleConsoleCommand; 060import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; 061 062/** 063 * A conlet for poll administration. 064 */ 065@SuppressWarnings("PMD.DataflowAnomalyAnalysis") 066public class LoginConlet extends FreeMarkerConlet<LoginConlet.AccountModel> { 067 068 private static final String PENDING_CONSOLE_PREPARED 069 = "pendingCosolePrepared"; 070 071 /** 072 * Creates a new component with its channel set to the given channel. 073 * 074 * @param componentChannel the channel that the component's handlers listen 075 * on by default and that {@link Manager#fire(Event, Channel...)} 076 * sends the event to 077 */ 078 public LoginConlet(Channel componentChannel) { 079 super(componentChannel); 080 } 081 082 @Override 083 protected String generateInstanceId(AddConletRequest event, 084 ConsoleConnection session) { 085 return "Singleton"; 086 } 087 088 /** 089 * Register conlet. 090 * 091 * @param event the event 092 * @param channel the channel 093 * @throws TemplateNotFoundException the template not found exception 094 * @throws MalformedTemplateNameException the malformed template name exception 095 * @throws ParseException the parse exception 096 * @throws IOException Signals that an I/O exception has occurred. 097 */ 098 @Handler 099 public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) 100 throws TemplateNotFoundException, MalformedTemplateNameException, 101 ParseException, IOException { 102 // Add conlet resources to page 103 channel.respond(new AddConletType(type()) 104 .addScript(new ScriptResource() 105 .setScriptUri(event.renderSupport().conletResource( 106 type(), "Login-functions.js")) 107 .setScriptType("module")) 108 .addCss(event.renderSupport(), WebConsoleUtils.uriFromPath( 109 "Login-style.css")) 110 .addPageContent("headerIcons", Map.of("priority", "1000")) 111 .addRenderMode(RenderMode.Content)); 112 } 113 114 /** 115 * As a model has already been created in {@link #doUpdateConletState}, 116 * the "new" model may already exist in the session. 117 */ 118 @Override 119 protected Optional<AccountModel> createNewState(AddConletRequest event, 120 ConsoleConnection session, String conletId) throws Exception { 121 Optional<AccountModel> model 122 = stateFromSession(session.session(), conletId); 123 if (model.isPresent()) { 124 return model; 125 } 126 return super.createNewState(event, session, conletId); 127 } 128 129 @Override 130 protected Optional<AccountModel> createStateRepresentation( 131 RenderConletRequestBase<?> event, 132 ConsoleConnection channel, String conletId) throws IOException { 133 return Optional.of(new AccountModel(conletId)); 134 } 135 136 /** 137 * Handle web console page loaded. 138 * 139 * @param event the event 140 * @param channel the channel 141 * @throws IOException 142 * @throws ParseException 143 * @throws MalformedTemplateNameException 144 * @throws TemplateNotFoundException 145 */ 146 @Handler(priority = 1000) 147 public void onConsolePrepared(ConsolePrepared event, 148 ConsoleConnection channel) 149 throws TemplateNotFoundException, MalformedTemplateNameException, 150 ParseException, IOException { 151 // If we are logged in, proceed 152 if (channel.session().containsKey(Subject.class)) { 153 return; 154 } 155 156 // Suspend handling and save event "in" channel. 157 event.suspendHandling(); 158 channel.setAssociated(PENDING_CONSOLE_PREPARED, event); 159 160 // Create model and save in session. 161 String conletId = type() + TYPE_INSTANCE_SEPARATOR + "Singleton"; 162 AccountModel accountModel = new AccountModel(conletId); 163 accountModel.setDialogOpen(true); 164 putInSession(channel.session(), conletId, accountModel); 165 166 // Render login dialog 167 Template tpl = freemarkerConfig().getTemplate("Login-dialog.ftl.html"); 168 var bundle = resourceBundle(channel.locale()); 169 channel.respond(new OpenModalDialog(type(), conletId, 170 processTemplate(event, tpl, 171 fmSessionModel(channel.session()))) 172 .addOption("title", bundle.getString("title")) 173 .addOption("cancelable", false).addOption("okayLabel", "") 174 .addOption("applyLabel", bundle.getString("Submit")) 175 .addOption("useSubmit", true)); 176 } 177 178 private Future<String> processTemplate( 179 Event<?> request, Template template, 180 Object dataModel) { 181 return request.processedBy().map(procBy -> procBy.executorService()) 182 .orElse(Components.defaultExecutorService()).submit(() -> { 183 StringWriter out = new StringWriter(); 184 try { 185 template.process(dataModel, out); 186 } catch (TemplateException | IOException e) { 187 throw new IllegalArgumentException(e); 188 } 189 return out.toString(); 190 191 }); 192 } 193 194 @Override 195 protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, 196 ConsoleConnection channel, String conletId, 197 AccountModel model) throws Exception { 198 Set<RenderMode> renderedAs = new HashSet<>(); 199 if (event.renderAs().contains(RenderMode.Content)) { 200 Template tpl 201 = freemarkerConfig().getTemplate("Login-status.ftl.html"); 202 channel.respond(new RenderConlet(type(), conletId, 203 processTemplate(event, tpl, 204 fmModel(event, channel, conletId, model))) 205 .setRenderAs(RenderMode.Content)); 206 channel.respond(new NotifyConletView(type(), conletId, 207 "updateUser", 208 WebConsoleUtils.userFromSession(channel.session()) 209 .map(ConsoleUser::getDisplayName).orElse(null))); 210 renderedAs.add(RenderMode.Content); 211 } 212 return renderedAs; 213 } 214 215 @Override 216 protected void doUpdateConletState(NotifyConletModel event, 217 ConsoleConnection connection, AccountModel model) throws Exception { 218 var bundle = resourceBundle(connection.locale()); 219 if ("loginData".equals(event.method())) { 220 String userName = event.params().asString(0); 221 if (userName == null || userName.isEmpty()) { 222 connection.respond(new NotifyConletView(type(), 223 model.getConletId(), "setMessages", 224 null, bundle.getString("emptyUserName"))); 225 return; 226 } 227 model.setDialogOpen(false); 228 Subject user = new Subject(); 229 user.getPrincipals().add(new ConsoleUser(userName, userName)); 230 connection.session().put(Subject.class, user); 231 connection.respond(new CloseModalDialog(type(), event.conletId())); 232 connection 233 .associated(PENDING_CONSOLE_PREPARED, ConsolePrepared.class) 234 .ifPresentOrElse(ConsolePrepared::resumeHandling, 235 () -> connection 236 .respond(new SimpleConsoleCommand("reload"))); 237 return; 238 } 239 if ("logout".equals(event.method())) { 240 connection.responsePipeline() 241 .fire(new Close(), connection.upstreamChannel()).get(); 242 connection.close(); 243 connection.respond(new DiscardSession(connection.session(), 244 connection.webletChannel())); 245 // Alternative to sending Close (see above): 246 // channel.respond(new SimpleConsoleCommand("reload")); 247 } 248 } 249 250 @Override 251 protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, 252 String conletId) throws Exception { 253 return stateFromSession(channel.session(), 254 type() + TYPE_INSTANCE_SEPARATOR + "Singleton") 255 .map(model -> !model.isDialogOpen()).orElse(true); 256 } 257 258 /** 259 * Model with account info. 260 */ 261 public static class AccountModel extends ConletBaseModel { 262 263 private boolean dialogOpen; 264 265 /** 266 * Creates a new model with the given type and id. 267 * 268 * @param conletId the web console component id 269 */ 270 @ConstructorProperties({ "conletId" }) 271 public AccountModel(String conletId) { 272 super(conletId); 273 } 274 275 /** 276 * Checks if is dialog open. 277 * 278 * @return true, if is dialog open 279 */ 280 public boolean isDialogOpen() { 281 return dialogOpen; 282 } 283 284 /** 285 * Sets the dialog open. 286 * 287 * @param dialogOpen the new dialog open 288 */ 289 public void setDialogOpen(boolean dialogOpen) { 290 this.dialogOpen = dialogOpen; 291 } 292 293 } 294 295}