mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 12:23:03 +00:00
Implement playground access control with configuration, authentication, and UI
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
package cn.qaiu.vx.core.verticle.conf;
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.core.json.JsonArray;
|
||||||
|
import io.vertx.core.json.impl.JsonUtil;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter and mapper for {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf}.
|
||||||
|
* NOTE: This class has been automatically generated from the {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf} original class using Vert.x codegen.
|
||||||
|
*/
|
||||||
|
public class HttpProxyConfConverter {
|
||||||
|
|
||||||
|
|
||||||
|
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
|
||||||
|
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
|
||||||
|
|
||||||
|
static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpProxyConf obj) {
|
||||||
|
for (java.util.Map.Entry<String, Object> member : json) {
|
||||||
|
switch (member.getKey()) {
|
||||||
|
case "password":
|
||||||
|
if (member.getValue() instanceof String) {
|
||||||
|
obj.setPassword((String)member.getValue());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "port":
|
||||||
|
if (member.getValue() instanceof Number) {
|
||||||
|
obj.setPort(((Number)member.getValue()).intValue());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "preProxyOptions":
|
||||||
|
if (member.getValue() instanceof JsonObject) {
|
||||||
|
obj.setPreProxyOptions(new io.vertx.core.net.ProxyOptions((io.vertx.core.json.JsonObject)member.getValue()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "timeout":
|
||||||
|
if (member.getValue() instanceof Number) {
|
||||||
|
obj.setTimeout(((Number)member.getValue()).intValue());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "username":
|
||||||
|
if (member.getValue() instanceof String) {
|
||||||
|
obj.setUsername((String)member.getValue());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void toJson(HttpProxyConf obj, JsonObject json) {
|
||||||
|
toJson(obj, json.getMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void toJson(HttpProxyConf obj, java.util.Map<String, Object> json) {
|
||||||
|
if (obj.getPassword() != null) {
|
||||||
|
json.put("password", obj.getPassword());
|
||||||
|
}
|
||||||
|
if (obj.getPort() != null) {
|
||||||
|
json.put("port", obj.getPort());
|
||||||
|
}
|
||||||
|
if (obj.getPreProxyOptions() != null) {
|
||||||
|
json.put("preProxyOptions", obj.getPreProxyOptions().toJson());
|
||||||
|
}
|
||||||
|
if (obj.getTimeout() != null) {
|
||||||
|
json.put("timeout", obj.getTimeout());
|
||||||
|
}
|
||||||
|
if (obj.getUsername() != null) {
|
||||||
|
json.put("username", obj.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,36 @@ import axios from 'axios';
|
|||||||
* 演练场API服务
|
* 演练场API服务
|
||||||
*/
|
*/
|
||||||
export const playgroundApi = {
|
export const playgroundApi = {
|
||||||
|
/**
|
||||||
|
* 获取Playground状态
|
||||||
|
* @returns {Promise} 状态信息 {enabled, needPassword, authed}
|
||||||
|
*/
|
||||||
|
async getStatus() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/v2/playground/status');
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.msg || error.message || '获取状态失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playground登录
|
||||||
|
* @param {string} password - 密码
|
||||||
|
* @returns {Promise} 登录结果
|
||||||
|
*/
|
||||||
|
async login(password) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/v2/playground/login', { password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.msg || error.message || '登录失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
* @param {string} jsCode - JavaScript代码
|
* @param {string} jsCode - JavaScript代码
|
||||||
|
|||||||
@@ -1,6 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
||||||
<el-card class="playground-card">
|
<!-- 加载状态 -->
|
||||||
|
<el-card v-if="statusLoading" class="playground-card" v-loading="true" element-loading-text="正在加载...">
|
||||||
|
<div style="height: 400px;"></div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- Playground未开启 -->
|
||||||
|
<el-card v-else-if="!enabled" class="playground-card">
|
||||||
|
<el-empty description="Playground未开启">
|
||||||
|
<template #extra>
|
||||||
|
<p style="color: #909399; font-size: 14px; margin-top: 10px;">
|
||||||
|
Playground功能目前未启用,请联系管理员在配置中开启此功能。
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</el-empty>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 需要密码但未认证 -->
|
||||||
|
<el-card v-else-if="needPassword && !authed" class="playground-card">
|
||||||
|
<div class="password-container">
|
||||||
|
<h2>🔒 Playground访问认证</h2>
|
||||||
|
<p style="color: #909399; margin: 20px 0;">
|
||||||
|
此Playground需要密码访问,请输入密码后继续使用。
|
||||||
|
</p>
|
||||||
|
<el-form @submit.prevent="submitPassword" style="max-width: 400px; margin: 0 auto;">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入访问密码"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
@keyup.enter="submitPassword"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="authError" style="margin-bottom: 10px;">
|
||||||
|
<el-alert type="error" :title="authError" :closable="false" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
style="width: 100%;"
|
||||||
|
:loading="authenticating"
|
||||||
|
@click="submitPassword"
|
||||||
|
>
|
||||||
|
{{ authenticating ? '验证中...' : '验证并进入' }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 已启用且已认证(或公开模式) -->
|
||||||
|
<el-card v-else class="playground-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
@@ -505,6 +563,15 @@ export default {
|
|||||||
const loadingList = ref(false);
|
const loadingList = ref(false);
|
||||||
const publishDialogVisible = ref(false);
|
const publishDialogVisible = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
|
|
||||||
|
// Playground状态相关
|
||||||
|
const statusLoading = ref(true);
|
||||||
|
const enabled = ref(false);
|
||||||
|
const needPassword = ref(false);
|
||||||
|
const authed = ref(false);
|
||||||
|
const password = ref('');
|
||||||
|
const authError = ref('');
|
||||||
|
const authenticating = ref(false);
|
||||||
const publishForm = ref({
|
const publishForm = ref({
|
||||||
jsCode: ''
|
jsCode: ''
|
||||||
});
|
});
|
||||||
@@ -656,6 +723,63 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取Playground状态
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
try {
|
||||||
|
const result = await playgroundApi.getStatus();
|
||||||
|
enabled.value = result.enabled;
|
||||||
|
needPassword.value = result.needPassword;
|
||||||
|
authed.value = result.authed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取Playground状态失败:', error);
|
||||||
|
ElMessage.error('获取Playground状态失败: ' + error.message);
|
||||||
|
// 默认为未启用
|
||||||
|
enabled.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交密码
|
||||||
|
const submitPassword = async () => {
|
||||||
|
if (!password.value) {
|
||||||
|
authError.value = '请输入密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authError.value = '';
|
||||||
|
authenticating.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await playgroundApi.login(password.value);
|
||||||
|
if (result.success || result.code === 200) {
|
||||||
|
authed.value = true;
|
||||||
|
ElMessage.success('认证成功');
|
||||||
|
// 初始化Playground
|
||||||
|
await nextTick();
|
||||||
|
await initPlayground();
|
||||||
|
} else {
|
||||||
|
authError.value = result.msg || '密码错误';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Playground登录失败:', error);
|
||||||
|
authError.value = error.message || '登录失败';
|
||||||
|
} finally {
|
||||||
|
authenticating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化Playground(加载编辑器等)
|
||||||
|
const initPlayground = async () => {
|
||||||
|
await initMonacoTypes();
|
||||||
|
|
||||||
|
// 加载保存的代码
|
||||||
|
const saved = localStorage.getItem('playground_code');
|
||||||
|
if (saved) {
|
||||||
|
jsCode.value = saved;
|
||||||
|
} else {
|
||||||
|
jsCode.value = exampleCode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 代码变化处理
|
// 代码变化处理
|
||||||
const onCodeChange = (value) => {
|
const onCodeChange = (value) => {
|
||||||
jsCode.value = value;
|
jsCode.value = value;
|
||||||
@@ -1156,14 +1280,19 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
checkDarkMode();
|
checkDarkMode();
|
||||||
await initMonacoTypes();
|
|
||||||
|
|
||||||
// 加载保存的代码
|
// 首先获取Playground状态
|
||||||
const saved = localStorage.getItem('playground_code');
|
await fetchStatus();
|
||||||
if (saved) {
|
statusLoading.value = false;
|
||||||
jsCode.value = saved;
|
|
||||||
} else {
|
// 如果未启用,直接返回
|
||||||
jsCode.value = exampleCode;
|
if (!enabled.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不需要密码或已认证,初始化Playground
|
||||||
|
if (!needPassword.value || authed.value) {
|
||||||
|
await initPlayground();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载保存的主题
|
// 加载保存的主题
|
||||||
@@ -1244,6 +1373,17 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
helpCollapseActive,
|
helpCollapseActive,
|
||||||
consoleLogs,
|
consoleLogs,
|
||||||
clearConsoleLogs,
|
clearConsoleLogs,
|
||||||
|
// Playground状态相关
|
||||||
|
statusLoading,
|
||||||
|
enabled,
|
||||||
|
needPassword,
|
||||||
|
authed,
|
||||||
|
password,
|
||||||
|
authError,
|
||||||
|
authenticating,
|
||||||
|
fetchStatus,
|
||||||
|
submitPassword,
|
||||||
|
initPlayground,
|
||||||
// 新增功能
|
// 新增功能
|
||||||
collapsedPanels,
|
collapsedPanels,
|
||||||
togglePanel,
|
togglePanel,
|
||||||
@@ -1266,6 +1406,27 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Password container styles */
|
||||||
|
.password-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-container h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .password-container {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
/* API示例对话框样式 */
|
/* API示例对话框样式 */
|
||||||
.api-example-dialog {
|
.api-example-dialog {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cn.qaiu.lz;
|
|||||||
import cn.qaiu.WebClientVertxInit;
|
import cn.qaiu.WebClientVertxInit;
|
||||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||||
|
import cn.qaiu.lz.common.config.PlaygroundConfig;
|
||||||
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
||||||
import cn.qaiu.vx.core.Deploy;
|
import cn.qaiu.vx.core.Deploy;
|
||||||
import cn.qaiu.vx.core.util.ConfigConstant;
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
@@ -88,5 +89,10 @@ public class AppMain {
|
|||||||
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
||||||
localMap.put(ConfigConstant.AUTHS, auths);
|
localMap.put(ConfigConstant.AUTHS, auths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Playground配置
|
||||||
|
if (jsonObject.containsKey("playground")) {
|
||||||
|
PlaygroundConfig.init(jsonObject.getJsonObject("playground"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package cn.qaiu.lz.common.config;
|
||||||
|
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playground配置加载器
|
||||||
|
*
|
||||||
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
|
*/
|
||||||
|
public class PlaygroundConfig {
|
||||||
|
private static boolean enabled = false;
|
||||||
|
private static String password = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置
|
||||||
|
* @param config 配置对象
|
||||||
|
*/
|
||||||
|
public static void init(JsonObject config) {
|
||||||
|
if (config == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enabled = config.getBoolean("enabled", false);
|
||||||
|
password = config.getString("password", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用Playground
|
||||||
|
*/
|
||||||
|
public static boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取密码
|
||||||
|
*/
|
||||||
|
public static String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要密码
|
||||||
|
*/
|
||||||
|
public static boolean hasPassword() {
|
||||||
|
return StringUtils.isNotBlank(password);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package cn.qaiu.lz.web.controller;
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.lz.common.config.PlaygroundConfig;
|
||||||
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
import cn.qaiu.parser.ParserCreate;
|
import cn.qaiu.parser.ParserCreate;
|
||||||
@@ -13,6 +14,7 @@ import cn.qaiu.vx.core.enums.RouteMethod;
|
|||||||
import cn.qaiu.vx.core.model.JsonResult;
|
import cn.qaiu.vx.core.model.JsonResult;
|
||||||
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||||
import cn.qaiu.vx.core.util.ResponseUtil;
|
import cn.qaiu.vx.core.util.ResponseUtil;
|
||||||
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.http.HttpServerRequest;
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
@@ -42,8 +44,154 @@ public class PlaygroundApi {
|
|||||||
|
|
||||||
private static final int MAX_PARSER_COUNT = 100;
|
private static final int MAX_PARSER_COUNT = 100;
|
||||||
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
||||||
|
private static final String AUTH_SESSION_KEY = "playground_authed_";
|
||||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Playground状态
|
||||||
|
*
|
||||||
|
* @param ctx 路由上下文
|
||||||
|
* @return 状态信息
|
||||||
|
*/
|
||||||
|
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
||||||
|
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
||||||
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean enabled = PlaygroundConfig.isEnabled();
|
||||||
|
boolean needPassword = enabled && PlaygroundConfig.hasPassword();
|
||||||
|
boolean authed = false;
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
if (!needPassword) {
|
||||||
|
// 公开模式,无需认证
|
||||||
|
authed = true;
|
||||||
|
} else {
|
||||||
|
// 检查是否已认证
|
||||||
|
String clientId = getClientId(ctx.request());
|
||||||
|
authed = isAuthenticated(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = new JsonObject()
|
||||||
|
.put("enabled", enabled)
|
||||||
|
.put("needPassword", needPassword)
|
||||||
|
.put("authed", authed);
|
||||||
|
|
||||||
|
promise.complete(JsonResult.data(result).toJsonObject());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取Playground状态失败", e);
|
||||||
|
promise.complete(JsonResult.error("获取状态失败: " + e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playground登录
|
||||||
|
*
|
||||||
|
* @param ctx 路由上下文
|
||||||
|
* @return 登录结果
|
||||||
|
*/
|
||||||
|
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
||||||
|
public Future<JsonObject> login(RoutingContext ctx) {
|
||||||
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!PlaygroundConfig.isEnabled()) {
|
||||||
|
promise.complete(JsonResult.error("Playground未开启").toJsonObject());
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PlaygroundConfig.hasPassword()) {
|
||||||
|
// 无密码模式,直接标记为已认证
|
||||||
|
String clientId = getClientId(ctx.request());
|
||||||
|
setAuthenticated(clientId, true);
|
||||||
|
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
|
String password = body.getString("password");
|
||||||
|
|
||||||
|
if (StringUtils.equals(password, PlaygroundConfig.getPassword())) {
|
||||||
|
String clientId = getClientId(ctx.request());
|
||||||
|
setAuthenticated(clientId, true);
|
||||||
|
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
||||||
|
} else {
|
||||||
|
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Playground登录失败", e);
|
||||||
|
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Playground是否启用和已认证
|
||||||
|
*/
|
||||||
|
private void ensurePlaygroundAccess(RoutingContext ctx) {
|
||||||
|
if (!PlaygroundConfig.isEnabled()) {
|
||||||
|
throw new IllegalStateException("Playground未开启");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlaygroundConfig.hasPassword()) {
|
||||||
|
String clientId = getClientId(ctx.request());
|
||||||
|
if (!isAuthenticated(clientId)) {
|
||||||
|
throw new IllegalStateException("未授权访问Playground");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端唯一标识
|
||||||
|
*/
|
||||||
|
private String getClientId(HttpServerRequest request) {
|
||||||
|
// 优先使用Cookie中的session id,否则使用IP
|
||||||
|
String cookie = request.getHeader("Cookie");
|
||||||
|
if (cookie != null && cookie.contains("playground_session=")) {
|
||||||
|
String sessionId = cookie.substring(cookie.indexOf("playground_session=") + 19);
|
||||||
|
int endIndex = sessionId.indexOf(";");
|
||||||
|
if (endIndex > 0) {
|
||||||
|
sessionId = sessionId.substring(0, endIndex);
|
||||||
|
}
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
return getClientIp(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已认证
|
||||||
|
*/
|
||||||
|
private boolean isAuthenticated(String clientId) {
|
||||||
|
try {
|
||||||
|
Boolean authed = (Boolean) VertxHolder.getVertxInstance()
|
||||||
|
.sharedData()
|
||||||
|
.getLocalMap("playground_auth")
|
||||||
|
.get(AUTH_SESSION_KEY + clientId);
|
||||||
|
return authed != null && authed;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("检查认证状态失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置认证状态
|
||||||
|
*/
|
||||||
|
private void setAuthenticated(String clientId, boolean authed) {
|
||||||
|
try {
|
||||||
|
VertxHolder.getVertxInstance()
|
||||||
|
.sharedData()
|
||||||
|
.getLocalMap("playground_auth")
|
||||||
|
.put(AUTH_SESSION_KEY + clientId, authed);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设置认证状态失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
*
|
*
|
||||||
@@ -55,6 +203,9 @@ public class PlaygroundApi {
|
|||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
String shareUrl = body.getString("shareUrl");
|
String shareUrl = body.getString("shareUrl");
|
||||||
@@ -220,11 +371,16 @@ public class PlaygroundApi {
|
|||||||
* 获取types.js文件内容
|
* 获取types.js文件内容
|
||||||
*
|
*
|
||||||
* @param response HTTP响应
|
* @param response HTTP响应
|
||||||
|
* @param ctx 路由上下文
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
||||||
public void getTypesJs(HttpServerResponse response) {
|
public void getTypesJs(HttpServerResponse response, RoutingContext ctx) {
|
||||||
try (InputStream inputStream = getClass().getClassLoader()
|
try {
|
||||||
.getResourceAsStream("custom-parsers/types.js")) {
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
|
try (InputStream inputStream = getClass().getClassLoader()
|
||||||
|
.getResourceAsStream("custom-parsers/types.js")) {
|
||||||
|
|
||||||
if (inputStream == null) {
|
if (inputStream == null) {
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
|
||||||
@@ -238,6 +394,10 @@ public class PlaygroundApi {
|
|||||||
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
|
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
|
||||||
.end(content);
|
.end(content);
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("访问Playground失败", e);
|
||||||
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.getMessage()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("读取types.js失败", e);
|
log.error("读取types.js失败", e);
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
|
||||||
@@ -248,8 +408,15 @@ public class PlaygroundApi {
|
|||||||
* 获取解析器列表
|
* 获取解析器列表
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserList() {
|
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
||||||
return dbService.getPlaygroundParserList();
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
return dbService.getPlaygroundParserList();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取解析器列表失败", e);
|
||||||
|
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,6 +427,9 @@ public class PlaygroundApi {
|
|||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
|
|
||||||
@@ -359,6 +529,9 @@ public class PlaygroundApi {
|
|||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
|
|
||||||
@@ -410,16 +583,30 @@ public class PlaygroundApi {
|
|||||||
* 删除解析器
|
* 删除解析器
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
||||||
public Future<JsonObject> deleteParser(Long id) {
|
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
||||||
return dbService.deletePlaygroundParser(id);
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
return dbService.deletePlaygroundParser(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除解析器失败", e);
|
||||||
|
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取解析器
|
* 根据ID获取解析器
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserById(Long id) {
|
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
||||||
return dbService.getPlaygroundParserById(id);
|
try {
|
||||||
|
// 检查访问权限
|
||||||
|
ensurePlaygroundAccess(ctx);
|
||||||
|
return dbService.getPlaygroundParserById(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取解析器失败", e);
|
||||||
|
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,4 +101,12 @@ auths:
|
|||||||
# 123网盘:配置用户名密码
|
# 123网盘:配置用户名密码
|
||||||
ye:
|
ye:
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
|
|
||||||
|
# Playground演练场配置
|
||||||
|
playground:
|
||||||
|
# 是否启用Playground,默认关闭
|
||||||
|
enabled: false
|
||||||
|
# 访问密码,可选。仅在enabled=true时生效
|
||||||
|
# 为空时表示公开访问,不需要密码
|
||||||
|
password: ""
|
||||||
Reference in New Issue
Block a user