Add playground loading animation, password auth, and mobile layout support

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-07 05:20:06 +00:00
parent 9c121c03f2
commit 5fbbe5b240
6 changed files with 664 additions and 55 deletions

View File

@@ -4,6 +4,7 @@ import cn.qaiu.WebClientVertxInit;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
import cn.qaiu.lz.web.config.PlaygroundConfig;
import cn.qaiu.vx.core.Deploy;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder;
@@ -88,5 +89,8 @@ public class AppMain {
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
localMap.put(ConfigConstant.AUTHS, auths);
}
// 演练场配置
PlaygroundConfig.loadFromJson(jsonObject);
}
}

View File

@@ -0,0 +1,73 @@
package cn.qaiu.lz.web.config;
import io.vertx.core.json.JsonObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* JS演练场配置
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@Data
@Slf4j
public class PlaygroundConfig {
/**
* 单例实例
*/
private static PlaygroundConfig instance;
/**
* 是否公开模式(不需要密码)
* 默认false需要密码访问
*/
private boolean isPublic = false;
/**
* 访问密码
* 默认密码nfd_playground_2024
*/
private String password = "nfd_playground_2024";
/**
* 私有构造函数
*/
private PlaygroundConfig() {
}
/**
* 获取单例实例
*/
public static PlaygroundConfig getInstance() {
if (instance == null) {
synchronized (PlaygroundConfig.class) {
if (instance == null) {
instance = new PlaygroundConfig();
}
}
}
return instance;
}
/**
* 从JsonObject加载配置
*/
public static void loadFromJson(JsonObject config) {
PlaygroundConfig cfg = getInstance();
if (config != null && config.containsKey("playground")) {
JsonObject playgroundConfig = config.getJsonObject("playground");
cfg.isPublic = playgroundConfig.getBoolean("public", false);
cfg.password = playgroundConfig.getString("password", "nfd_playground_2024");
log.info("Playground配置已加载: public={}, password={}",
cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
}
} else {
log.info("未找到playground配置使用默认值: public=false");
}
}
}

View File

@@ -1,6 +1,7 @@
package cn.qaiu.lz.web.controller;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.web.config.PlaygroundConfig;
import cn.qaiu.lz.web.model.PlaygroundTestResp;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.ParserCreate;
@@ -19,6 +20,7 @@ import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -42,7 +44,92 @@ public class PlaygroundApi {
private static final int MAX_PARSER_COUNT = 100;
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/**
* 检查Playground访问权限
*/
private boolean checkAuth(RoutingContext ctx) {
PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接允许访问
if (config.isPublic()) {
return true;
}
// 否则检查Session中的认证状态
Session session = ctx.session();
if (session == null) {
return false;
}
Boolean authed = session.get(SESSION_AUTH_KEY);
return authed != null && authed;
}
/**
* 获取Playground状态是否需要认证
*/
@RouteMapping(value = "/status", method = RouteMethod.GET)
public Future<JsonObject> getStatus(RoutingContext ctx) {
PlaygroundConfig config = PlaygroundConfig.getInstance();
boolean authed = checkAuth(ctx);
JsonObject result = new JsonObject()
.put("public", config.isPublic())
.put("authed", authed);
return Future.succeededFuture(JsonResult.ok(result).toJsonObject());
}
/**
* Playground登录
*/
@RouteMapping(value = "/login", method = RouteMethod.POST)
public Future<JsonObject> login(RoutingContext ctx) {
Promise<JsonObject> promise = Promise.promise();
try {
PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接成功
if (config.isPublic()) {
Session session = ctx.session();
if (session != null) {
session.put(SESSION_AUTH_KEY, true);
}
promise.complete(JsonResult.ok("公开模式,无需密码").toJsonObject());
return promise.future();
}
// 获取密码
JsonObject body = ctx.body().asJsonObject();
String password = body.getString("password");
if (StringUtils.isBlank(password)) {
promise.complete(JsonResult.error("密码不能为空").toJsonObject());
return promise.future();
}
// 验证密码
if (config.getPassword().equals(password)) {
Session session = ctx.session();
if (session != null) {
session.put(SESSION_AUTH_KEY, true);
}
promise.complete(JsonResult.ok("登录成功").toJsonObject());
} else {
promise.complete(JsonResult.error("密码错误").toJsonObject());
}
} catch (Exception e) {
log.error("登录失败", e);
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
}
return promise.future();
}
/**
* 测试执行JavaScript代码
@@ -52,6 +139,11 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/test", method = RouteMethod.POST)
public Future<JsonObject> test(RoutingContext ctx) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
@@ -248,7 +340,11 @@ public class PlaygroundApi {
* 获取解析器列表
*/
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
public Future<JsonObject> getParserList() {
public Future<JsonObject> getParserList(RoutingContext ctx) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
return dbService.getPlaygroundParserList();
}
@@ -257,6 +353,11 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
public Future<JsonObject> saveParser(RoutingContext ctx) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
@@ -356,6 +457,11 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
@@ -410,7 +516,11 @@ public class PlaygroundApi {
* 删除解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
public Future<JsonObject> deleteParser(Long id) {
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
return dbService.deletePlaygroundParser(id);
}
@@ -418,7 +528,11 @@ public class PlaygroundApi {
* 根据ID获取解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
public Future<JsonObject> getParserById(Long id) {
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
return dbService.getPlaygroundParserById(id);
}
@@ -427,6 +541,11 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/typescript", method = RouteMethod.POST)
public Future<JsonObject> saveTypeScriptCode(RoutingContext ctx) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
@@ -489,7 +608,11 @@ public class PlaygroundApi {
* 根据parserId获取TypeScript代码
*/
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.GET)
public Future<JsonObject> getTypeScriptCode(Long parserId) {
public Future<JsonObject> getTypeScriptCode(RoutingContext ctx, Long parserId) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
return dbService.getTypeScriptCodeByParserId(parserId);
}
@@ -498,6 +621,11 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/typescript/:parserId", method = RouteMethod.PUT)
public Future<JsonObject> updateTypeScriptCode(RoutingContext ctx, Long parserId) {
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {

View File

@@ -13,6 +13,13 @@ server:
# 反向代理服务器配置路径(不用加后缀)
proxyConf: server-proxy
# JS演练场配置
playground:
# 公开模式默认false需要密码访问设为true则无需密码
public: false
# 访问密码,建议修改默认密码!
password: 'nfd_playground_2024'
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
vertx:
eventLoopPoolSize: 0