Projects tigase _server tigase-http-api Commits 39c9f90f
ctrl k
  • pom.xml
    ■ ■ ■ ■ ■
    skipped 200 lines
    201 201   <dependency>
    202 202   <groupId>tigase</groupId>
    203 203   <artifactId>tigase-server</artifactId>
    204  - <version>8.2.0</version>
     204 + <version>8.4.0-SNAPSHOT</version>
    205 205   <scope>compile</scope>
    206 206   </dependency>
    207 207   <dependency>
    208 208   <groupId>tigase</groupId>
    209 209   <artifactId>tigase-server</artifactId>
    210  - <version>8.2.0</version>
     210 + <version>8.4.0-SNAPSHOT</version>
    211 211   <type>test-jar</type>
    212 212   <scope>test</scope>
    213 213   </dependency>
    skipped 54 lines
    268 268   <artifactId>jakarta.ws.rs-api</artifactId>
    269 269   <version>3.0.0</version>
    270 270   <scope>compile</scope>
     271 + </dependency>
     272 + <dependency>
     273 + <groupId>com.google.zxing</groupId>
     274 + <artifactId>core</artifactId>
     275 + <version>3.5.3</version>
     276 + </dependency>
     277 + <dependency>
     278 + <groupId>com.google.zxing</groupId>
     279 + <artifactId>javase</artifactId>
     280 + <version>3.5.3</version>
    271 281   </dependency>
    272 282   <dependency>
    273 283   <groupId>mysql</groupId>
    skipped 55 lines
  • src/main/java/tigase/http/modules/dashboard/DashboardHandler.java
    ■ ■ ■ ■
    skipped 31 lines
    32 32  public abstract class DashboardHandler implements Handler {
    33 33   
    34 34   protected TemplateEngine engine;
    35  - @ConfigField(desc = "Path to template files")
     35 + @ConfigField(desc = "Path to template files", alias = "templatesPath")
    36 36   private String templatesPath;
    37 37  
    38 38   DashboardHandler() {
    skipped 26 lines
  • src/main/java/tigase/http/modules/dashboard/UsersHandler.java
    ■ ■ ■ ■ ■ ■
    skipped 16 lines
    17 17   */
    18 18  package tigase.http.modules.dashboard;
    19 19   
     20 +import com.google.zxing.BarcodeFormat;
     21 +import com.google.zxing.EncodeHintType;
     22 +import com.google.zxing.WriterException;
     23 +import com.google.zxing.client.j2se.MatrixToImageConfig;
     24 +import com.google.zxing.client.j2se.MatrixToImageWriter;
     25 +import com.google.zxing.common.BitMatrix;
     26 +import com.google.zxing.qrcode.QRCodeWriter;
    20 27  import jakarta.ws.rs.*;
    21 28  import jakarta.ws.rs.core.MediaType;
    22 29  import jakarta.ws.rs.core.Response;
    23 30  import jakarta.ws.rs.core.UriInfo;
     31 +import tigase.auth.credentials.entries.XTokenCredentialsEntry;
     32 +import tigase.auth.mechanisms.SaslXTOKEN;
    24 33  import tigase.db.AuthRepository;
    25 34  import tigase.db.TigaseDBException;
    26 35  import tigase.db.UserRepository;
    skipped 2 lines
    29 38  import tigase.http.jaxrs.Pageable;
    30 39  import tigase.kernel.beans.Bean;
    31 40  import tigase.kernel.beans.Inject;
     41 +import tigase.util.Base64;
    32 42  import tigase.util.stringprep.TigaseStringprepException;
    33 43  import tigase.vhosts.VHostManager;
    34 44  import tigase.xmpp.jid.BareJID;
    skipped 1 lines
    36 46   
    37 47  import javax.validation.constraints.NotBlank;
    38 48  import javax.validation.constraints.NotEmpty;
     49 +import java.io.ByteArrayOutputStream;
     50 +import java.io.IOException;
     51 +import java.nio.charset.StandardCharsets;
     52 +import java.security.SecureRandom;
    39 53  import java.util.*;
    40 54   
    41 55  @Bean(name = "users", parent = DashboardModule.class, active = true)
    42 56  @Path("/users")
    43 57  public class UsersHandler extends DashboardHandler {
    44 58   
     59 + private final SecureRandom secureRandom = new SecureRandom();
    45 60   @Inject
    46 61   private AuthRepository authRepository;
    47 62   @Inject
    skipped 42 lines
    90 105   model.put("query", query);
    91 106   model.put("users", new Page<>(pageable, jids.size(), users));
    92 107   model.put("domains", domains);
     108 + 
     109 + model.put("isXTokenActive", authRepository.isMechanismSupported("default", SaslXTOKEN.NAME));
     110 + 
    93 111   String output = renderTemplate("users/index.jte", model);
    94 112   return Response.ok(output, MediaType.TEXT_HTML).build();
    95 113   }
    skipped 56 lines
    152 170  
    153 171   public static Response redirectToIndex(UriInfo uriInfo, String query) {
    154 172   return Response.seeOther(uriInfo.getBaseUriBuilder().path(UsersHandler.class, "index").replaceQueryParam("query", query).build()).build();
     173 + }
     174 + 
     175 + @POST
     176 + @Path("/{jid}/qrCode")
     177 + @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     178 + @Produces("image/png")
     179 + public Response generateAuthQrCode(@PathParam("jid") @NotEmpty BareJID jid)
     180 + throws IOException, WriterException, TigaseDBException {
     181 + String token = generateAuthQrCodeToken(jid);
     182 + 
     183 + QRCodeWriter qrCodeWriter = new QRCodeWriter();
     184 + BitMatrix bitMatrix = qrCodeWriter.encode(token.toString(), BarcodeFormat.QR_CODE, 300, 300, Map.of(
     185 + EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8, EncodeHintType.MARGIN, 0));
     186 + MatrixToImageConfig imageConfig = new MatrixToImageConfig(MatrixToImageConfig.BLACK, MatrixToImageConfig.WHITE);
     187 + ByteArrayOutputStream baos = new ByteArrayOutputStream();
     188 + MatrixToImageWriter.writeToStream(bitMatrix, "PNG", baos, imageConfig);
     189 + return Response.ok(baos.toByteArray(), "image/png").build();
     190 + }
     191 + 
     192 + private String generateAuthQrCodeToken(BareJID jid) throws TigaseDBException {
     193 + byte[] secret = new byte[32];
     194 + secureRandom.nextBytes(secret);
     195 + 
     196 + byte[] jidBytes = jid.toString().getBytes(StandardCharsets.UTF_8);
     197 + byte[] data = new byte[secret.length + 1 + jidBytes.length];
     198 + System.arraycopy(secret, 0, data, 0, secret.length);
     199 + System.arraycopy(jidBytes, 0, data, secret.length + 1, jidBytes.length);
     200 + String token = Base64.encode(data);
     201 + 
     202 + authRepository.removeCredential(jid, "default");
     203 + authRepository.updateCredential(jid, "default", SaslXTOKEN.NAME, new XTokenCredentialsEntry(secret, true).encoded());
     204 + return token;
    155 205   }
    156 206   
    157 207   public record User(BareJID jid, AuthRepository.AccountStatus accountStatus) {}
    skipped 2 lines
  • src/main/resources/tigase/dashboard/users/index.jte
    ■ ■ ■ ■ ■ ■
    skipped 12 lines
    13 13  @param List<String> domains
    14 14  @param String query
    15 15  @param HttpServletRequest request
     16 +@param boolean isXTokenActive
    16 17   
    17 18  @template.layout(securityContext = securityContext, uriInfo = uriInfo, request = request, content = @`
    18 19   <div class="card m-3 bg-white">
    skipped 57 lines
    76 77   <i class="bi bi-safe2-fill me-2"></i>Change password
    77 78   </a>
    78 79   </li>
     80 + @if(isXTokenActive)
     81 + <li>
     82 + <a class="dropdown-item text-warning text-nowrap" href="#" data-bs-toggle="modal"
     83 + data-bs-target="#generateAuthToken-${user.jid().toString().replace('@','-').replace('.','-')}">
     84 + <i class="bi bi-qr-code me-2"></i>New QR code
     85 + </a>
     86 + </li>
     87 + @endif
    79 88   <li>
    80 89   <a class="dropdown-item text-danger text-nowrap" href="#" data-bs-toggle="modal"
    81 90   data-bs-target="#deleteUser-${user.jid().toString().replace('@','-').replace('.','-')}">
    skipped 51 lines
    133 142   </div>
    134 143   </div>
    135 144   </div>
     145 + @if(isXTokenActive)
     146 + <div class="modal fade" id="generateAuthToken-${user.jid().toString().replace('@','-').replace('.','-')}" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="#generateAuthTokenModalLabel-${user.toString().replace('@','-').replace('.','-')}" aria-hidden="true">
     147 + <div class="modal-dialog">
     148 + <div class="modal-content">
     149 + <form action="${uriInfo.getBaseUriBuilder().path(UsersHandler.class,"generateAuthQrCode").build(user.jid().toString()).toString()}" method="post">
     150 + <div class="modal-header">
     151 + <h1 class="modal-title fs-5" id="generateAuthTokenModalLabel-${user.jid().toString().replace('@','-').replace('.','-')}">Generate new token</h1>
     152 + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
     153 + </div>
     154 + <div class="modal-body">
     155 + <p>Do you wish to generate a new authentication token for user ${user.jid().toString()}?</p>
     156 + </div>
     157 + <div class="modal-footer">
     158 + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
     159 + <a type="submit" class="btn btn-primary" onclick="generateQrCode('${user.jid().toString()}')">Generate</a>
     160 + </div>
     161 + </form>
     162 + </div>
     163 + </div>
     164 + </div>
     165 + @endif
    136 166   </td>
    137 167   </tr>
    138 168   @endfor
    139 169   </tbody>
     170 + @if(isXTokenActive)
     171 + <script>
     172 + let generateQrCode = function (jid) {
     173 + let escapedJid = jid.replace("@","-").replace(".","-");
     174 + let modalBody = document.querySelector("#generateAuthToken-" + escapedJid + " div.modal-body");
     175 + let generateButton = document.querySelector("#generateAuthToken-" + escapedJid + " div.modal-footer a[type='submit']");
     176 + // remove existing error message
     177 + document.querySelector("#generateAuthToken-" + escapedJid + " div.modal-body span.error-message")?.remove();
     178 + // update "generate" button label
     179 + let originalGenerateButtonLabel = generateButton.textContent;
     180 + generateButton.classList.add("disabled");
     181 + generateButton.innerHTML = "";
     182 + let spinner = document.createElement("div");
     183 + spinner.setAttribute("role", "status");
     184 + spinner.classList.add("spinner-border", "spinner-border-sm", "me-2");
     185 + generateButton.append(spinner);
     186 + generateButton.append(document.createTextNode("Processing..."));
     187 + // send request
     188 + let xhr = new XMLHttpRequest();
     189 + let url = "${uriInfo.getBaseUriBuilder().path(UsersHandler.class,"generateAuthQrCode").build("@").toString()}".replace("@", jid);
     190 + xhr.open("POST", url, true);
     191 + xhr.setRequestHeader('Content-type', "application/x-www-form-urlencoded");
     192 + xhr.send("");
     193 + xhr.responseType = "blob";
     194 + let handleResult = function () {
     195 + // update generate button
     196 + spinner.remove();
     197 + generateButton.classList.remove("disabled");
     198 + generateButton.innerHTML = "";
     199 + generateButton.append(document.createTextNode(originalGenerateButtonLabel));
     200 + if (xhr.status === 200) {
     201 + // replace content of modal
     202 + modalBody.innerHTML = "";
     203 + let image = new Image();
     204 + image.classList.add("d-block", "mx-auto", "w-50");
     205 + let qrCodeImageDataUrl = URL.createObjectURL(xhr.response);
     206 + image.src = qrCodeImageDataUrl;
     207 + modalBody.append(image);
     208 + let p = document.createElement("p");
     209 + p.classList.add("text-secondary", "small", "w-100", "text-center", "m-0", "pt-1")
     210 + p.append(document.createTextNode("Scan QR code to authenticate account."))
     211 + modalBody.append(p);
     212 + // replace generate button with "save" button
     213 + let modalFooter = generateButton.parentElement;
     214 + generateButton.remove();
     215 + let saveLink = document.createElement("a");
     216 + saveLink.classList.add("btn", "btn-secondary");
     217 + saveLink.append(document.createTextNode("Save to file"))
     218 + saveLink.href = qrCodeImageDataUrl;
     219 + saveLink.setAttribute("download", "qrcode-" + jid + ".png");
     220 + modalFooter.append(saveLink);
     221 + } else {
     222 + // show error
     223 + let errorSpan = document.createElement("span");
     224 + errorSpan.append(document.createTextNode("An error occurred. Please try again later."));
     225 + errorSpan.classList.add("text-danger", "error-message");
     226 + modalBody.append(errorSpan);
     227 + }
     228 + }
     229 + xhr.onerror = handleResult;
     230 + xhr.onload = handleResult;
     231 + }
     232 + </script>
     233 + @endif
    140 234   </table>
    141 235   @template.pagination(uriInfo = uriInfo, page = users)
    142 236   </div>
    skipped 2 lines
Please wait...
Page is in error, reload to recover