mirror of
https://github.com/opsre/LiteOps.git
synced 2026-02-26 01:50:46 +08:00
✨ feat: 新增水印功能
This commit is contained in:
@@ -353,6 +353,11 @@ class SecurityConfig(models.Model):
|
||||
max_login_attempts = models.IntegerField(default=5, verbose_name='最大登录尝试次数')
|
||||
lockout_duration = models.IntegerField(default=30, verbose_name='账户锁定时间(分钟)')
|
||||
enable_2fa = models.BooleanField(default=False, verbose_name='启用双因子认证')
|
||||
# 水印配置
|
||||
watermark_enabled = models.BooleanField(default=True, verbose_name='启用水印')
|
||||
watermark_content = models.TextField(default='胡图图不涂涂', verbose_name='水印内容')
|
||||
watermark_show_time = models.BooleanField(default=False, verbose_name='显示时间水印')
|
||||
watermark_show_username = models.BooleanField(default=False, verbose_name='显示用户名水印')
|
||||
update_time = models.DateTimeField(auto_now=True, null=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -43,6 +43,10 @@ class SecurityConfigView(View):
|
||||
'max_login_attempts': security_config.max_login_attempts,
|
||||
'lockout_duration': security_config.lockout_duration,
|
||||
'enable_2fa': security_config.enable_2fa,
|
||||
'watermark_enabled': security_config.watermark_enabled,
|
||||
'watermark_content': security_config.watermark_content,
|
||||
'watermark_show_time': security_config.watermark_show_time,
|
||||
'watermark_show_username': security_config.watermark_show_username,
|
||||
'update_time': security_config.update_time.strftime('%Y-%m-%d %H:%M:%S') if security_config.update_time else None
|
||||
}
|
||||
})
|
||||
@@ -67,6 +71,10 @@ class SecurityConfigView(View):
|
||||
max_login_attempts = data.get('max_login_attempts')
|
||||
lockout_duration = data.get('lockout_duration')
|
||||
enable_2fa = data.get('enable_2fa')
|
||||
watermark_enabled = data.get('watermark_enabled')
|
||||
watermark_content = data.get('watermark_content')
|
||||
watermark_show_time = data.get('watermark_show_time')
|
||||
watermark_show_username = data.get('watermark_show_username')
|
||||
|
||||
# 验证输入数据
|
||||
if min_password_length is not None:
|
||||
@@ -111,6 +119,18 @@ class SecurityConfigView(View):
|
||||
'message': '账户锁定时间必须在5-60分钟之间'
|
||||
})
|
||||
|
||||
if watermark_content is not None:
|
||||
if not isinstance(watermark_content, str) or len(watermark_content.strip()) == 0:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '水印内容不能为空'
|
||||
})
|
||||
if len(watermark_content) > 500:
|
||||
return JsonResponse({
|
||||
'code': 400,
|
||||
'message': '水印内容长度不能超过500字符'
|
||||
})
|
||||
|
||||
# 获取或创建安全配置
|
||||
security_config, created = SecurityConfig.objects.get_or_create(id=1)
|
||||
|
||||
@@ -127,6 +147,14 @@ class SecurityConfigView(View):
|
||||
security_config.lockout_duration = lockout_duration
|
||||
if enable_2fa is not None:
|
||||
security_config.enable_2fa = enable_2fa
|
||||
if watermark_enabled is not None:
|
||||
security_config.watermark_enabled = watermark_enabled
|
||||
if watermark_content is not None:
|
||||
security_config.watermark_content = watermark_content
|
||||
if watermark_show_time is not None:
|
||||
security_config.watermark_show_time = watermark_show_time
|
||||
if watermark_show_username is not None:
|
||||
security_config.watermark_show_username = watermark_show_username
|
||||
|
||||
security_config.save()
|
||||
|
||||
@@ -364,6 +392,73 @@ def cleanup_login_logs(request):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'清理登录日志失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["GET"])
|
||||
def get_watermark_config(request):
|
||||
"""获取水印配置"""
|
||||
try:
|
||||
# 获取安全配置
|
||||
security_config, created = SecurityConfig.objects.get_or_create(
|
||||
id=1,
|
||||
defaults={
|
||||
'min_password_length': 8,
|
||||
'password_complexity': ['lowercase', 'number'],
|
||||
'session_timeout': 120,
|
||||
'max_login_attempts': 5,
|
||||
'lockout_duration': 30,
|
||||
'enable_2fa': False
|
||||
}
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取水印配置成功',
|
||||
'data': {
|
||||
'watermark_enabled': security_config.watermark_enabled,
|
||||
'watermark_content': security_config.watermark_content,
|
||||
'watermark_show_time': security_config.watermark_show_time,
|
||||
'watermark_show_username': security_config.watermark_show_username
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'获取水印配置失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@jwt_auth_required
|
||||
@require_http_methods(["GET"])
|
||||
def get_current_user_info(request):
|
||||
"""获取当前用户信息"""
|
||||
try:
|
||||
user = User.objects.get(user_id=request.user_id)
|
||||
|
||||
return JsonResponse({
|
||||
'code': 200,
|
||||
'message': '获取用户信息成功',
|
||||
'data': {
|
||||
'username': user.username,
|
||||
'name': user.name or user.username
|
||||
}
|
||||
})
|
||||
|
||||
except User.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'code': 404,
|
||||
'message': '用户不存在'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'获取用户信息失败: {str(e)}', exc_info=True)
|
||||
return JsonResponse({
|
||||
'code': 500,
|
||||
'message': f'服务器错误: {str(e)}'
|
||||
|
||||
@@ -15,7 +15,7 @@ from apps.views.logs import login_logs_list, login_log_detail
|
||||
from apps.views.dashboard import DashboardStatsView, BuildTrendView, BuildDetailView, RecentBuildsView, ProjectDistributionView
|
||||
from apps.views.webhook import GitLabWebhookView
|
||||
|
||||
from apps.views.security import SecurityConfigView, get_build_tasks_for_cleanup, cleanup_build_logs, cleanup_login_logs
|
||||
from apps.views.security import SecurityConfigView, get_build_tasks_for_cleanup, cleanup_build_logs, cleanup_login_logs, get_watermark_config, get_current_user_info
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
@@ -71,4 +71,6 @@ urlpatterns = [
|
||||
path('api/system/security/build-tasks/', get_build_tasks_for_cleanup, name='build-tasks-for-cleanup'),
|
||||
path('api/system/security/cleanup-build-logs/', cleanup_build_logs, name='cleanup-build-logs'),
|
||||
path('api/system/security/cleanup-login-logs/', cleanup_login_logs, name='cleanup-login-logs'),
|
||||
path('api/system/watermark/', get_watermark_config, name='watermark-config'),
|
||||
path('api/user/current/', get_current_user_info, name='current-user-info'),
|
||||
]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 363 KiB |
@@ -262,6 +262,10 @@ CREATE TABLE `security_config` (
|
||||
`lockout_duration` int NOT NULL,
|
||||
`enable_2fa` tinyint(1) NOT NULL,
|
||||
`update_time` datetime(6) DEFAULT NULL,
|
||||
`watermark_content` longtext COLLATE utf8mb4_bin NOT NULL,
|
||||
`watermark_enabled` tinyint(1) NOT NULL,
|
||||
`watermark_show_time` tinyint(1) NOT NULL,
|
||||
`watermark_show_username` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
@@ -356,4 +360,6 @@ INSERT INTO `role` (`id`, `role_id`, `name`, `description`, `permissions`, `crea
|
||||
-- 插入用户角色关联数据
|
||||
INSERT INTO `user_role` (`id`, `create_time`, `update_time`, `role_id`, `user_id`) VALUES (1, '2025-03-27 14:45:11.269249', '2025-03-27 14:45:11.269261', '333ec25423e04a4e96b4bb238de51cc3', '9bfef5a1ee1d4054be9727934ad112es');
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
-- 基本设置数据
|
||||
INSERT INTO `security_config` (`id`, `min_password_length`, `password_complexity`, `session_timeout`, `max_login_attempts`, `lockout_duration`, `enable_2fa`, `update_time`, `watermark_content`, `watermark_enabled`, `watermark_show_time`, `watermark_show_username`) VALUES (1, 6, '[\"number\", \"lowercase\"]', 120, 7, 5, 0, '2025-07-22 09:47:37.053022', '胡图图不涂涂', 0, 0, 0);
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
124
web/src/App.vue
124
web/src/App.vue
@@ -1,18 +1,138 @@
|
||||
<template>
|
||||
<a-config-provider :locale="zhCN" :theme="theme">
|
||||
<a-watermark
|
||||
v-if="shouldShowWatermark"
|
||||
:content="watermarkContent"
|
||||
>
|
||||
<a-config-provider :locale="zhCN" :theme="theme">
|
||||
<router-view></router-view>
|
||||
</a-config-provider>
|
||||
</a-watermark>
|
||||
<a-config-provider
|
||||
v-else
|
||||
:locale="zhCN"
|
||||
:theme="theme"
|
||||
>
|
||||
<router-view></router-view>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed, onMounted, watch, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import zhCN from "ant-design-vue/es/locale/zh_CN";
|
||||
import axios from 'axios';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// 配置 Ant Design Vue 主题,强制使用自定义 PingFang 字体
|
||||
const theme = {
|
||||
token: {
|
||||
fontFamily: "'PingFangCustom', Arial, sans-serif",
|
||||
},
|
||||
};
|
||||
|
||||
// 水印配置
|
||||
const watermarkConfig = reactive({
|
||||
enabled: false,
|
||||
content: '',
|
||||
showTime: false,
|
||||
showUsername: false
|
||||
});
|
||||
|
||||
// 用户信息
|
||||
const userInfo = reactive({
|
||||
username: '',
|
||||
name: ''
|
||||
});
|
||||
|
||||
// 当前日期(年月日)
|
||||
const currentDate = ref('');
|
||||
|
||||
const shouldShowWatermark = computed(() => {
|
||||
// 排除登陆页面显示水印
|
||||
if (route.path === '/login') {
|
||||
return false;
|
||||
}
|
||||
return watermarkConfig.enabled;
|
||||
});
|
||||
|
||||
const watermarkContent = computed(() => {
|
||||
const contents = [];
|
||||
|
||||
// 添加自定义水印内容
|
||||
if (watermarkConfig.content) {
|
||||
const customContents = watermarkConfig.content.split('\n').filter(line => line.trim());
|
||||
contents.push(...customContents);
|
||||
}
|
||||
|
||||
// 添加日期水印
|
||||
if (watermarkConfig.showTime && currentDate.value) {
|
||||
contents.push(currentDate.value);
|
||||
}
|
||||
|
||||
// 添加用户名水印
|
||||
if (watermarkConfig.showUsername && userInfo.name) {
|
||||
contents.push(userInfo.name);
|
||||
}
|
||||
|
||||
return contents.length > 0 ? contents : [];
|
||||
});
|
||||
|
||||
// 更新日期(年月日)
|
||||
const updateDate = () => {
|
||||
const now = new Date();
|
||||
currentDate.value = now.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 获取水印配置
|
||||
const fetchWatermarkConfig = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/system/watermark/');
|
||||
if (response.data.code === 200) {
|
||||
watermarkConfig.enabled = response.data.data.watermark_enabled;
|
||||
watermarkConfig.content = response.data.data.watermark_content;
|
||||
watermarkConfig.showTime = response.data.data.watermark_show_time;
|
||||
watermarkConfig.showUsername = response.data.data.watermark_show_username;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取水印配置失败:', error);
|
||||
// 使用默认配置
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const response = await axios.get('/api/user/current/', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
if (response.data.code === 200) {
|
||||
userInfo.username = response.data.data.username;
|
||||
userInfo.name = response.data.data.name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化,在非登录页面获取用户信息
|
||||
watch(() => route.path, (newPath) => {
|
||||
if (newPath !== '/login') {
|
||||
fetchUserInfo();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
fetchWatermarkConfig();
|
||||
|
||||
updateDate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -94,6 +94,71 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<a-divider>水印配置</a-divider>
|
||||
|
||||
<!-- 水印功能 -->
|
||||
<a-form layout="vertical" v-if="hasFunctionPermission('system_basic', 'edit')">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="启用水印">
|
||||
<a-switch
|
||||
v-model:checked="securityConfig.watermark_enabled"
|
||||
checked-children="开启"
|
||||
un-checked-children="关闭"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
开启后将在页面显示水印
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="显示时间">
|
||||
<a-switch
|
||||
v-model:checked="securityConfig.watermark_show_time"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:disabled="!securityConfig.watermark_enabled"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
在水印中显示当天日期
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="显示用户名">
|
||||
<a-switch
|
||||
v-model:checked="securityConfig.watermark_show_username"
|
||||
checked-children="显示"
|
||||
un-checked-children="隐藏"
|
||||
:disabled="!securityConfig.watermark_enabled"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
在水印中显示当前用户名
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="自定义水印内容">
|
||||
<a-textarea
|
||||
v-model:value="securityConfig.watermark_content"
|
||||
placeholder="请输入水印内容,支持多行"
|
||||
:rows="4"
|
||||
:maxLength="500"
|
||||
show-count
|
||||
:disabled="!securityConfig.watermark_enabled"
|
||||
/>
|
||||
<div class="form-item-help">
|
||||
支持多行文本,每行一个水印。时间和用户名水印会自动添加到自定义内容中
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<a-divider>日志清理</a-divider>
|
||||
|
||||
@@ -391,7 +456,11 @@ const securityConfig = reactive({
|
||||
session_timeout: 120,
|
||||
max_login_attempts: 5,
|
||||
lockout_duration: 30,
|
||||
enable_2fa: false
|
||||
enable_2fa: false,
|
||||
watermark_enabled: false,
|
||||
watermark_content: '',
|
||||
watermark_show_time: false,
|
||||
watermark_show_username: false
|
||||
});
|
||||
|
||||
const securityRules = {
|
||||
|
||||
Reference in New Issue
Block a user