From 0a8be8ac95a6460b0bd8db19139d78ac902e5c05 Mon Sep 17 00:00:00 2001 From: kl <632104866@QQ.com> Date: Fri, 11 Aug 2023 16:39:07 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6=E7=9A=84=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E9=80=BB=E8=BE=91=20(#479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 重构验证码删除文件的实现逻辑 * 移除未使用的依赖 * 微调描述信息 --- .../java/cn/keking/utils/CaptchaUtil.java | 89 ++++++++++++++++++ .../main/java/cn/keking/utils/DateUtils.java | 26 ++++++ .../keking/utils/RandomValidateCodeUtil.java | 90 ------------------- .../main/java/cn/keking/utils/WebUtils.java | 56 ++++++++++-- .../keking/web/controller/FileController.java | 84 ++++++++++++----- .../controller/OnlinePreviewController.java | 88 +----------------- server/src/main/resources/web/main/index.ftl | 10 +-- 7 files changed, 230 insertions(+), 213 deletions(-) create mode 100644 server/src/main/java/cn/keking/utils/CaptchaUtil.java create mode 100644 server/src/main/java/cn/keking/utils/DateUtils.java delete mode 100644 server/src/main/java/cn/keking/utils/RandomValidateCodeUtil.java diff --git a/server/src/main/java/cn/keking/utils/CaptchaUtil.java b/server/src/main/java/cn/keking/utils/CaptchaUtil.java new file mode 100644 index 00000000..168cbaba --- /dev/null +++ b/server/src/main/java/cn/keking/utils/CaptchaUtil.java @@ -0,0 +1,89 @@ +package cn.keking.utils; + +import com.aspose.cad.Tuple; +import org.springframework.util.ObjectUtils; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class CaptchaUtil { + + public static final String captcha_code = "captchaCode"; + public static final String captcha_code_pic = "captchaCodePic"; + public static final String captcha_generate_time = "captchaTime"; + + private static final int width = 100;// 定义图片的width + private static final int height = 30;// 定义图片的height + private static final int codeLength = 4;// 定义图片上显示验证码的个数 + private static final int xx = 18; + private static final int fontHeight = 28; + private static final int codeY = 27; + private static final char[] codeSequence = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', '2', '3', '4', '5', '6', '7', '8', '9'}; + + /** + * 指定验证码、或生成验证码。 + * @param captchaCode 指定验证码, 如果为 null,则生成验证码。否则,使用指定的验证码。 + * @return 验证码和验证码图片 + */ + public static Map generateCaptcha(String captchaCode) { + // 定义图像buffer + BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics gd = buffImg.getGraphics(); + Random random = new Random(); + // 将图像填充为白色 + gd.setColor(Color.WHITE); + gd.fillRect(0, 0, width, height); + Font font = new Font("Times New Roman", Font.BOLD, fontHeight); + gd.setFont(font); + // 画边框。 + gd.setColor(Color.BLACK); + gd.drawRect(0, 0, width - 1, height - 1); + + // 随机产生40条干扰线,使图象中的认证码不易被其它程序探测到。 + gd.setColor(Color.BLACK); + for (int i = 0; i < 30; i++) { + int x = random.nextInt(width); + int y = random.nextInt(height); + int xl = random.nextInt(12); + int yl = random.nextInt(12); + gd.drawLine(x, y, x + xl, y + yl); + } + // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。 + int red, green, blue; + + if (ObjectUtils.isEmpty(captchaCode)) { + captchaCode = generateCaptchaCode(); + } + + // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。 + red = random.nextInt(255); + green = random.nextInt(255); + blue = random.nextInt(255); + // 用随机产生的颜色将验证码绘制到图像中。 + gd.setColor(new Color(red, green, blue)); + gd.drawString(captchaCode, 18, codeY); + + Map map = new HashMap<>(); + map.put(captcha_code, captchaCode); + //存放生成的验证码BufferedImage对象 + map.put(captcha_code_pic, buffImg); + return map; + } + + /** + * 生成随机字符串。 + * @return 字符串 + */ + private static String generateCaptchaCode() { + Random random = new Random(); + StringBuilder randomCode = new StringBuilder(); + for (int i = 0; i < codeLength; i++) { + randomCode.append(codeSequence[random.nextInt(52)]); + } + return randomCode.toString(); + } +} diff --git a/server/src/main/java/cn/keking/utils/DateUtils.java b/server/src/main/java/cn/keking/utils/DateUtils.java new file mode 100644 index 00000000..84436a47 --- /dev/null +++ b/server/src/main/java/cn/keking/utils/DateUtils.java @@ -0,0 +1,26 @@ +package cn.keking.utils; + +import java.time.Instant; + +/** + * @author kl (http://kailing.pub) + * @since 2023/8/11 + */ +public class DateUtils { + /** + * 获取当前时间的秒级时间戳 + * @return + */ + public static long getCurrentSecond() { + return Instant.now().getEpochSecond(); + } + + /** + * 计算当前时间与指定时间的秒级时间戳差值 + * @param datetime 指定时间 + * @return 差值 + */ + public static long calculateCurrentTimeDifference(long datetime) { + return getCurrentSecond() - datetime; + } +} diff --git a/server/src/main/java/cn/keking/utils/RandomValidateCodeUtil.java b/server/src/main/java/cn/keking/utils/RandomValidateCodeUtil.java deleted file mode 100644 index 84db4dc4..00000000 --- a/server/src/main/java/cn/keking/utils/RandomValidateCodeUtil.java +++ /dev/null @@ -1,90 +0,0 @@ -package cn.keking.utils; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; - -public class RandomValidateCodeUtil { - - private static final int width = 100;// 定义图片的width - private static final int height = 30;// 定义图片的height - private static final int codeCount = 4;// 定义图片上显示验证码的个数 - private static final int xx = 18; - private static final int fontHeight = 28; - private static final int codeY = 27; - private static final char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R','T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a','b','c','d','e','f','g','h','j','k','m','n','p','q','r','s','t','u','v','w','x','y', '2', '3', '4','5', '6', '7', '8', '9' }; - - /** - * 生成一个map集合 - * code为生成的验证码 - * codePic为生成的验证码BufferedImage对象 - */ - public static Map generateCodeAndPic(String ip, String sessionCode, int lx) { - // 定义图像buffer - BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - // Graphics2D gd = buffImg.createGraphics(); - // Graphics2D gd = (Graphics2D) buffImg.getGraphics(); - Graphics gd = buffImg.getGraphics(); - // 创建一个随机数生成器类 - Random random = new Random(); - // 将图像填充为白色 - gd.setColor(Color.WHITE); - gd.fillRect(0, 0, width, height); - - // 创建字体,字体的大小应该根据图片的高度来定。 - Font font = new Font("Times New Roman", Font.BOLD, fontHeight); - // 设置字体。 - gd.setFont(font); - - // 画边框。 - gd.setColor(Color.BLACK); - gd.drawRect(0, 0, width - 1, height - 1); - - // 随机产生40条干扰线,使图象中的认证码不易被其它程序探测到。 - gd.setColor(Color.BLACK); - for (int i = 0; i < 30; i++) { - int x = random.nextInt(width); - int y = random.nextInt(height); - int xl = random.nextInt(12); - int yl = random.nextInt(12); - gd.drawLine(x, y, x + xl, y + yl); - } - StringBuffer randomCode = new StringBuffer(); - Map map = new HashMap<>(); - // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。 - int red, green, blue; - if (lx ==1){ - // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。 - red = random.nextInt(255); - green = random.nextInt(255); - blue = random.nextInt(255); - // 用随机产生的颜色将验证码绘制到图像中。 - gd.setColor(new Color(red, green, blue)); - gd.drawString(sessionCode, 18, codeY); - randomCode.append(sessionCode); - }else { - // 随机产生codeCount数字的验证码。 - for (int i = 0; i < codeCount; i++) { - // 得到随机产生的验证码数字。 - String code = String.valueOf(codeSequence[random.nextInt(52)]); - // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。 - red = random.nextInt(255); - green = random.nextInt(255); - blue = random.nextInt(255); - // 用随机产生的颜色将验证码绘制到图像中。 - gd.setColor(new Color(red, green, blue)); - gd.drawString(code, (i + 1) * xx, codeY); - // 将产生的四个随机数组合在一起。 - randomCode.append(code); - } - } - //存放验证码 - map.put("code", randomCode); - //存放生成的验证码BufferedImage对象 - map.put("codePic", buffImg); - return map; - } -} diff --git a/server/src/main/java/cn/keking/utils/WebUtils.java b/server/src/main/java/cn/keking/utils/WebUtils.java index c8a85e24..f888fbd5 100644 --- a/server/src/main/java/cn/keking/utils/WebUtils.java +++ b/server/src/main/java/cn/keking/utils/WebUtils.java @@ -9,6 +9,8 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.util.HtmlUtils; import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; @@ -27,7 +29,7 @@ import java.util.regex.Pattern; public class WebUtils { private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class); - private static final String BASE64_MSG = "base64"; + private static final String BASE64_MSG = "base64"; /** * 获取标准的URL * @@ -205,13 +207,7 @@ public class WebUtils { Matcher matcher = pattern.matcher(url); return matcher.find(); } - public static boolean kuayu(String host, String wjl) { //查询域名是否相同 - if (wjl.contains(host)) { - return true; - }else { - return false; - } - } + /** * 将 Base64 字符串解码,再解码URL参数, 默认使用 UTF-8 * @param source 原始 Base64 字符串 @@ -265,4 +261,48 @@ public class WebUtils { } return null; } + + /** + * 获取 session 中的 String 属性 + * @param request 请求 + * @return 属性值 + */ + public static String getSessionAttr(HttpServletRequest request, String key) { + HttpSession session = request.getSession(); + if (session == null) { + return null; + } + Object value = session.getAttribute(key); + if (value == null) { + return null; + } + return value.toString(); + } + + /** + * 获取 session 中的 long 属性 + * @param request 请求 + * @param key 属性名 + * @return 属性值 + */ + public static long getLongSessionAttr(HttpServletRequest request, String key) { + String value = getSessionAttr(request, key); + if (value == null) { + return 0; + } + return Long.parseLong(value); + } + + /** + * 移除 session 中的属性 + * @param request 请求 + * @param key 属性名 + */ + public static void removeSessionAttr(HttpServletRequest request, String key) { + HttpSession session = request.getSession(); + if (session == null) { + return; + } + session.removeAttribute(key); + } } diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java index 5aa2074e..9f075be1 100644 --- a/server/src/main/java/cn/keking/web/controller/FileController.java +++ b/server/src/main/java/cn/keking/web/controller/FileController.java @@ -2,7 +2,9 @@ package cn.keking.web.controller; import cn.keking.config.ConfigConstants; import cn.keking.model.ReturnResponse; +import cn.keking.utils.DateUtils; import cn.keking.utils.KkFileUtils; +import cn.keking.utils.CaptchaUtil; import cn.keking.utils.RarUtils; import cn.keking.utils.WebUtils; import org.slf4j.Logger; @@ -11,11 +13,16 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StreamUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import javax.imageio.ImageIO; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -24,9 +31,11 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; +import static cn.keking.utils.CaptchaUtil.*; + /** * @author yudian-it - * 2017/12/1 + * 2017/12/1 */ @RestController public class FileController { @@ -35,6 +44,7 @@ public class FileController { private final String fileDir = ConfigConstants.getFileDir(); private final String demoDir = "demo"; + private final String demoPath = demoDir + File.separator; public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!"; @@ -61,28 +71,11 @@ public class FileController { @GetMapping("/deleteFile") public ReturnResponse deleteFile(HttpServletRequest request, String fileName, String password) { - ReturnResponse checkResult = this.deleteFileCheck(fileName); + ReturnResponse checkResult = this.deleteFileCheck(request, fileName, password); if (checkResult.isFailure()) { return checkResult; } - fileName = checkResult.getContent().toString(); - if(ConfigConstants.getDeleteCaptcha()){ - String sessionCode; - try { - sessionCode = request.getSession().getAttribute("code").toString(); //获取已经保存的验证码 - } catch (Exception e) { - sessionCode = "null"; - } - if (!sessionCode.equalsIgnoreCase(password)){ - logger.error("删除文件【{}】失败,密码错误!",fileName); - return ReturnResponse.failure("删除文件失败,密码错误!"); - } - }else { - if(!ConfigConstants.getPassword().equalsIgnoreCase(password)) { - logger.error("删除文件【{}】失败,密码错误!",fileName); - return ReturnResponse.failure("删除文件失败,密码错误!"); - } - } + fileName = checkResult.getContent().toString(); File file = new File(fileDir + demoPath + fileName); logger.info("删除文件:{}", file.getAbsolutePath()); if (file.exists() && !file.delete()) { @@ -90,10 +83,45 @@ public class FileController { logger.error(msg); return ReturnResponse.failure(msg); } - request.getSession().removeAttribute("code"); //删除缓存验证码 + WebUtils.removeSessionAttr(request, captcha_code); //删除缓存验证码 return ReturnResponse.success(); } + /** + * 验证码方法 + */ + @RequestMapping("/deleteFile/captcha") + public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (!ConfigConstants.getDeleteCaptcha()) { + return; + } + + response.setContentType("image/jpeg"); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setDateHeader("Expires", -1); + String captchaCode = WebUtils.getSessionAttr(request, captcha_code); + long captchaGenerateTime = WebUtils.getLongSessionAttr(request, captcha_generate_time); + long timeDifference = DateUtils.calculateCurrentTimeDifference(captchaGenerateTime); + + Map codeMap; + + // 验证码为空,且生成验证码超过50秒,重新生成验证码 + if (timeDifference > 50 && ObjectUtils.isEmpty(captchaCode)) { + codeMap = CaptchaUtil.generateCaptcha(null); + // 更新验证码 + request.getSession().setAttribute(captcha_code, codeMap.get(captcha_code).toString()); + request.getSession().setAttribute(captcha_generate_time, DateUtils.getCurrentSecond()); + } else { + captchaCode = ObjectUtils.isEmpty(captchaCode) ? "wait" : captchaCode; + codeMap = CaptchaUtil.generateCaptcha(captchaCode); + } + + ServletOutputStream sos = response.getOutputStream(); + ImageIO.write((RenderedImage) codeMap.get(captcha_code_pic), "jpeg", sos); + sos.close(); + } + @GetMapping("/listFiles") public List> getFiles() { List> list = new ArrayList<>(); @@ -121,7 +149,7 @@ public class FileController { return ReturnResponse.failure("文件传接口已禁用"); } String fileName = WebUtils.getFileNameFromMultipartFile(file); - if(fileName.lastIndexOf(".")==-1){ + if (fileName.lastIndexOf(".") == -1) { return ReturnResponse.failure("不允许上传的类型"); } if (!KkFileUtils.isAllowedUpload(fileName)) { @@ -144,7 +172,7 @@ public class FileController { * @param fileName 文件名 * @return 校验结果 */ - private ReturnResponse deleteFileCheck(String fileName) { + private ReturnResponse deleteFileCheck(HttpServletRequest request, String fileName, String password) { if (ObjectUtils.isEmpty(fileName)) { return ReturnResponse.failure("文件名为空,删除失败!"); } @@ -161,6 +189,16 @@ public class FileController { if (KkFileUtils.isIllegalFileName(fileName)) { return ReturnResponse.failure("非法文件名,删除失败!"); } + if (ObjectUtils.isEmpty(password)) { + return ReturnResponse.failure("密码 or 验证码为空,删除失败!"); + } + + String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, captcha_code) : ConfigConstants.getPassword(); + + if (!password.equalsIgnoreCase(expectedPassword)) { + logger.error("删除文件【{}】失败,密码错误!", fileName); + return ReturnResponse.failure("删除文件失败,密码错误!"); + } return ReturnResponse.success(fileName); } diff --git a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java index ca95d62a..1c3bf110 100644 --- a/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java +++ b/server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java @@ -1,6 +1,5 @@ package cn.keking.web.controller; -import cn.keking.config.ConfigConstants; import cn.keking.model.FileAttribute; import cn.keking.service.FileHandlerService; import cn.keking.service.FilePreview; @@ -8,7 +7,6 @@ import cn.keking.service.FilePreviewFactory; import cn.keking.service.cache.CacheService; import cn.keking.service.impl.OtherFilePreviewImpl; import cn.keking.utils.KkFileUtils; -import cn.keking.utils.RandomValidateCodeUtil; import cn.keking.utils.WebUtils; import fr.opensagres.xdocreport.core.io.IOUtils; import io.mola.galimatias.GalimatiasParseException; @@ -17,17 +15,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import javax.imageio.ImageIO; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.awt.image.RenderedImage; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; @@ -35,11 +28,8 @@ import java.net.HttpURLConnection; import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; import java.util.Arrays; -import java.util.Date; import java.util.List; -import java.util.Map; import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE; @@ -66,7 +56,7 @@ public class OnlinePreviewController { @GetMapping( "/onlinePreview") public String onlinePreview(String url, Model model, HttpServletRequest req) { - + String fileUrl; try { fileUrl = WebUtils.decodeUrl(url); @@ -199,82 +189,6 @@ public class OnlinePreviewController { } } } - /** - * 验证码方法 - */ - @RequestMapping("/captcha") - public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception { - if(!ConfigConstants.getDeleteCaptcha()){ - return; - } - response.setContentType("image/gif"); - response.setHeader("Pragma", "No-cache"); - response.setHeader("Cache-Control", "no-cache"); - response.setDateHeader("Expires", 0); - Date date = new Date(); // 当前时间 - SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 设置时间格式 - String sessionCode; - try { - sessionCode = request.getSession().getAttribute("code").toString(); //获取已经保存的验证码 - } catch (Exception e) { - sessionCode= null; - } - Object time = request.getSession().getAttribute("time"); //获取已经保存的时间 - if (ObjectUtils.isEmpty(time)){ //判断时间是否为空 - request.getSession().setAttribute("time", formater.format(date)); //为空重新添加缓存时间 - time = request.getSession().getAttribute("time"); - } - Date joinTime = formater.parse(String.valueOf(time)); - String dateStart = formater.format(joinTime); - Date d1=formater.parse(dateStart); - // 时间差: - long diff = date.getTime() - d1.getTime(); - long diffSeconds = diff / 1000 % 60; - String ip=request.getRemoteAddr(); - ServletOutputStream sos = null; - if (ObjectUtils.isEmpty(sessionCode) || diffSeconds > 50){ //判断验证码是否为空 为空重新生成 判断是否在有效时间内 默认50秒 - Map codeMap = RandomValidateCodeUtil.generateCodeAndPic(ip,sessionCode,0); - // 验证码存入session - request.getSession().setAttribute("code", codeMap.get("code").toString()); - // 时间存入session - request.getSession().setAttribute("time", formater.format(date)); - // 禁止图像缓存。 - response.setHeader("Pragma", "no-cache"); - response.setHeader("Cache-Control", "no-cache"); - response.setDateHeader("Expires", -1); - response.setContentType("image/jpeg"); - // 将图像输出到Servlet输出流中。 - try { - sos = response.getOutputStream(); - ImageIO.write((RenderedImage) codeMap.get("codePic"), "jpeg", sos); - } catch (IOException e) { - e.printStackTrace(); - } finally { - assert sos != null; - sos.close(); - } - }else { - // System.out.println("请输入你的姓名:"); - Map codeMap = RandomValidateCodeUtil.generateCodeAndPic(ip,sessionCode,1); - // 禁止图像缓存。 - response.setHeader("Pragma", "no-cache"); - response.setHeader("Cache-Control", "no-cache"); - response.setDateHeader("Expires", -1); - response.setContentType("image/jpeg"); - // 将图像输出到Servlet输出流中。 - try { - sos = response.getOutputStream(); - ImageIO.write((RenderedImage) codeMap.get("codePic"), "jpeg", sos); - } catch (IOException e) { - e.printStackTrace(); - } finally { - assert sos != null; - sos.close(); - } - - } - - } /** * 通过api接口入队 diff --git a/server/src/main/resources/web/main/index.ftl b/server/src/main/resources/web/main/index.ftl index b004dace..24951ba1 100644 --- a/server/src/main/resources/web/main/index.ftl +++ b/server/src/main/resources/web/main/index.ftl @@ -201,7 +201,7 @@