feat:用户管理

This commit is contained in:
xiaozzzi 2024-02-02 18:02:35 +08:00
parent 88c01f5fbb
commit f83660a738
34 changed files with 1025 additions and 62 deletions

View File

@ -1,9 +1,12 @@
package com.blossom.backend.base.auth;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.auth.pojo.LoginReq;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.auth.pojo.AccessToken;
import com.blossom.backend.base.auth.pojo.KickOutReq;
import com.blossom.backend.base.auth.pojo.LoginReq;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.pojo.R;
import lombok.AllArgsConstructor;
@ -52,6 +55,18 @@ public class AuthController {
return R.ok();
}
/**
* 踢出用户
*
* @since 1.13.0
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("kickout")
public R<?> kickout(@RequestBody KickOutReq req) {
authService.kickout(req.getUserId());
return R.ok();
}
/**
* 检查 Token 状态
*/

View File

@ -46,6 +46,7 @@ public class AuthService extends AbstractAuthService {
AuthException.throwBy(StrUtil.isBlank(login.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
UserEntity user = userService.selectByUsername(login.getUsername());
AuthException.throwBy(ObjUtil.isNull(user), AuthRCode.USERNAME_OR_PWD_FAULT);
AuthException.throwBy(user.getDelTime() == null || !user.getDelTime().equals(0L), AuthRCode.USER_NOT_ENABLED);
AuthException.throwBy(!passwordEncoder.matches(login.getPassword() + user.getSalt(), user.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
fillUserDetail(accessToken, user);
}
@ -59,6 +60,15 @@ public class AuthService extends AbstractAuthService {
tokenRepository.remove(token);
}
/**
* 踢出用户的所有令牌
*
* @param userId 用户ID
*/
public void kickout(Long userId) {
tokenRepository.removeAll(userId);
}
/**
* 用户注册
*/

View File

@ -0,0 +1,19 @@
package com.blossom.backend.base.auth.annotation;
import com.blossom.backend.base.user.UserTypeEnum;
import java.lang.annotation.*;
/**
* 接口校验用户类型
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthUserType {
/**
* 用户的类型, 接口只允许该类型用户调用
*/
UserTypeEnum value();
}

View File

@ -13,6 +13,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
@ -42,7 +43,7 @@ public class CaffeineTokenRepository implements TokenRepository {
.initialCapacity(1000)
.expireAfterWrite(client.getDuration(), TimeUnit.SECONDS)
.removalListener((String key, AccessToken value, RemovalCause cause) ->
log.info("Token [" + key + "] 被删除")
log.info("Token [" + key + "] has been deleted")
)
.build();
@ -50,9 +51,10 @@ public class CaffeineTokenRepository implements TokenRepository {
.initialCapacity(1000)
.expireAfterWrite(client.getDuration(), TimeUnit.SECONDS)
.removalListener((String userId, String token, RemovalCause cause) ->
log.info("Unique Token(userId) [" + userId + "] 被删除")
log.info("Unique Token(userId) [" + userId + "] has been deleted")
)
.build();
}
@Override
@ -78,6 +80,17 @@ public class CaffeineTokenRepository implements TokenRepository {
tokenCache.invalidate(token);
}
@Override
public void removeAll(Long userId) {
uniqueTokenCache.invalidate(userId);
Map<String, AccessToken> maps = tokenCache.asMap();
maps.forEach((k, t) -> {
if (t.getUserId().equals(userId)) {
tokenCache.invalidate(k);
}
});
}
@Override
public void saveUniqueToken(AccessToken accessToken) {
if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) {

View File

@ -27,7 +27,7 @@ public enum AuthRCode implements IRCode {
*/
USERNAME_OR_PWD_FAULT ("AUTH-40004", "用户名或密码错误","用户名或密码错误, 或用户名不存在。"),
CAPTCHA_FAULT ("AUTH-40005", "验证码错误","验证码错误, 或手机号不存在。"),
USER_NOT_ENABLED ("AUTH-40010", "用户已禁用, 暂时无法登录","用户已禁用, 暂时无法登录。"),
USER_NOT_ENABLED ("AUTH-40010", "您的账户已被已禁用, 暂时无法登录","您的账户已被已禁用, 暂时无法登录。"),
/**
* 401: 未经过认证

View File

@ -2,8 +2,10 @@ package com.blossom.backend.base.auth.interceptor;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.auth.exception.AuthException;
import com.blossom.backend.base.auth.exception.AuthRCode;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.common.base.exception.XzException400;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.web.method.HandlerMethod;
@ -14,7 +16,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 判断用户的类型, 只读用户只允许发送 get 请求
* 接口对于用户的校验
*/
@Slf4j
public class UserTypeInterceptor implements HandlerInterceptor {
@ -28,7 +30,7 @@ public class UserTypeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 忽略静态资源处理器
if(handler instanceof ResourceHttpRequestHandler) {
if (handler instanceof ResourceHttpRequestHandler) {
return true;
}
@ -40,12 +42,25 @@ public class UserTypeInterceptor implements HandlerInterceptor {
if (isIgnore) {
return true;
}
// GET请求校验
if (HttpMethod.GET.name().equals(request.getMethod())) {
return true;
// 校验接口允许的用户类型
boolean isCheckUserType = handlerMethod.hasMethodAnnotation(AuthUserType.class);
if (isCheckUserType) {
AuthUserType userType = handlerMethod.getMethodAnnotation(AuthUserType.class);
if (userType != null) {
UserTypeEnum type = userType.value();
if (!type.getType().equals(AuthContext.getType())) {
throw new AuthException(AuthRCode.PERMISSION_DENIED);
}
return true;
}
}
// 只读账号不非 GET 请求
if (UserTypeEnum.READONLY.getType().equals(AuthContext.getType()) && !HttpMethod.GET.name().equals(request.getMethod())) {
throw new AuthException(AuthRCode.PERMISSION_DENIED.getCode(), "您的账号为只读账号, 无法使用该功能");
}
XzException400.throwBy(UserTypeEnum.READONLY.getType().equals(AuthContext.getType()), "您的账号为只读账号, 无法使用该功能");
return true;
}
}

View File

@ -31,6 +31,10 @@ public class JWTTokenRepository implements TokenRepository {
}
@Override
public void removeAll(Long userId) {
}
@Override
public void saveUniqueToken(AccessToken accessToken) {

View File

@ -0,0 +1,15 @@
package com.blossom.backend.base.auth.pojo;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 踢出用户
*/
@Data
public class KickOutReq {
@NotNull(message = "userId 为必填项")
private Long userId;
}

View File

@ -31,6 +31,13 @@ public interface TokenRepository {
*/
void remove(String token);
/**
* 删除某个用户的所有 token
*
* @param userId 用户ID
*/
void removeAll(Long userId);
/**
* 保存唯一生效的 token 对象
*

View File

@ -1,7 +1,11 @@
package com.blossom.backend.base.paramu;
import cn.hutool.core.util.ObjUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.paramu.pojo.UserParamUpdReq;
import com.blossom.backend.base.user.UserTypeEnum;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.pojo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -45,6 +49,20 @@ public class UserParamController {
return R.ok(baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values()));
}
/**
* 修改用户参数
*
* @apiNote 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/upd/admin")
public R<Map<String, String>> updByAdmin(@Validated @RequestBody UserParamUpdReq req) {
XzException400.throwBy(ObjUtil.isNull(req.getUserId()), "用户ID为必填项");
baseService.update(req);
baseService.refresh();
return R.ok(baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values()));
}
/**
* 刷新用户配置
*/

View File

@ -2,6 +2,7 @@ package com.blossom.backend.base.sys;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.param.pojo.ParamUpdReq;
@ -63,6 +64,7 @@ public class SysController {
/**
* 修改系统参数
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/param/upd")
public R<Map<String, String>> upd(@Validated @RequestBody ParamUpdReq req) {
if (!UserTypeEnum.ADMIN.getType().equals(AuthContext.getType())) {
@ -76,6 +78,7 @@ public class SysController {
/**
* 刷新系统配置
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/param/refresh")
public R<?> paramRefresh() {
paramService.refresh();

View File

@ -2,7 +2,9 @@ package com.blossom.backend.base.user;
import cn.hutool.core.util.ObjUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.AuthService;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.auth.annotation.AuthUserType;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.paramu.UserParamEnum;
@ -12,14 +14,15 @@ import com.blossom.backend.base.user.pojo.*;
import com.blossom.backend.config.BlConstants;
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
import com.blossom.backend.server.article.stat.ArticleStatService;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.exception.XzException404;
import com.blossom.common.base.exception.XzException500;
import com.blossom.common.base.pojo.R;
import com.blossom.common.base.util.spring.SpringUtil;
import lombok.AllArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
@ -32,24 +35,84 @@ import java.util.Map;
@AllArgsConstructor
public class UserController {
private final UserService userService;
private final AuthService authService;
private final ArticleStatService articleService;
private final SysService sysService;
private final ParamService paramService;
private final UserParamService userParamService;
/**
* 获取用户列表
*
* @apiNote 需要管理员权限
* @since 1.13.0
*/
@AuthUserType(UserTypeEnum.ADMIN)
@GetMapping("/list")
public R<List<UserListRes>> list() {
return R.ok(userService.listAll(), UserListRes.class);
}
/**
* 用户信息
*
* @param id 用户ID
* @apiNote 根据用户ID获取用户信息, 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@GetMapping("/info/admin")
public R<BlossomUserRes> user(@RequestParam("id") Long id) {
return getUserById(id);
}
/**
* 新增用户
*
* @apiNote 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/add")
public R<?> add(@Validated @RequestBody UserAddReq req) {
userService.insert(req);
return R.ok();
}
/**
* 修改用户
*
* @apiNote 需要管理员权限
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/upd/admin")
public R<?> updateImportant(@Validated @RequestBody UserUpdAdminReq req) {
UserEntity user = req.to(UserEntity.class);
userService.updById(user);
return R.ok();
}
/**
* 禁用启用
*/
@AuthUserType(UserTypeEnum.ADMIN)
@PostMapping("/disabled")
public R<?> disabled(@Validated @RequestBody UserDisabledReq req) {
if (req.getId().equals(AuthContext.getUserId())) {
throw new XzException500("不能禁用自己");
}
UserEntity user = req.to(UserEntity.class);
userService.updById(user);
authService.kickout(req.getId());
return R.ok();
}
/**
* 用户信息
*
* @apiNote 当前登录用户的用户信息
*/
@GetMapping("/info")
public R<BlossomUserRes> user() {
BlossomUserRes user = userService.selectById(AuthContext.getUserId()).to(BlossomUserRes.class);
user.setOsRes(sysService.getOsConfig());
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
user.setParams(paramMap);
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
user.setUserParams(userParamMap);
return R.ok(user);
return getUserById(AuthContext.getUserId());
}
/**
@ -96,15 +159,19 @@ public class UserController {
}
/**
* 新增用户
* 获取用户信息
*
* @param userId 用户ID
*/
@PostMapping("/add")
public R<?> add(@Validated @RequestBody UserAddReq req) {
UserEntity curUser = userService.getById(AuthContext.getUserId());
if (curUser == null || !UserTypeEnum.ADMIN.getType().equals(curUser.getType())) {
throw new XzException400("您没有权限添加用户");
}
userService.insert(req);
return R.ok();
private R<BlossomUserRes> getUserById(Long userId) {
BlossomUserRes user = userService.selectById(userId).to(BlossomUserRes.class);
user.setOsRes(sysService.getOsConfig());
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
user.setParams(paramMap);
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
user.setUserParams(userParamMap);
return R.ok(user);
}
}

View File

@ -7,6 +7,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
/**
@ -56,6 +57,14 @@ public class BlossomUserRes extends AbstractPOJO implements Serializable {
* 文章字数
*/
private Integer articleWords;
/**
* 创建日期
*/
private Date creTime;
/**
* 逻辑删除, 目前用于禁用用户, 而不是删除
*/
private Long delTime;
/**
* 对象存储信息, 非登录状态不返回该字段

View File

@ -0,0 +1,20 @@
package com.blossom.backend.base.user.pojo;
import com.blossom.common.base.pojo.AbstractPOJO;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
public class UserDisabledReq extends AbstractPOJO {
/**
* 用户ID
*/
@NotNull(message = "用户ID为必填项")
private Long id;
/**
* 禁用状态
*/
private Long delTime;
}

View File

@ -8,6 +8,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* 用户
@ -66,5 +67,13 @@ public class UserEntity extends AbstractPOJO implements Serializable {
* 用户的位置
*/
private String location;
/**
* 创建日期
*/
private Date creTime;
/**
* 逻辑删除, 目前用于禁用用户, 而不是删除
*/
private Long delTime;
}

View File

@ -0,0 +1,37 @@
package com.blossom.backend.base.user.pojo;
import lombok.Data;
import java.util.Date;
@Data
public class UserListRes {
/**
* 用户ID
*/
private Long id;
/**
* 用户头像
*/
private String avatar;
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickName;
/**
* 用户类型
*/
private Integer type;
/**
* 创建日期
*/
private Date creTime;
/**
* 逻辑删除, 目前用于禁用用户, 而不是删除
*/
private Long delTime;
}

View File

@ -0,0 +1,27 @@
package com.blossom.backend.base.user.pojo;
import com.blossom.common.base.pojo.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户请求
*
* @author xzzz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserUpdAdminReq extends AbstractEntity {
/**
* 用户ID
*/
private Long id;
/**
* 和风天气的位置
*/
private String location;
/**
* 类型
*/
private Integer type;
}

View File

@ -37,6 +37,5 @@ public class UserUpdReq extends AbstractEntity {
/**
* 用户头像
*/
@NotBlank(message = "用户头像为必填项")
private String avatar;
}

View File

@ -24,6 +24,8 @@
<if test="avatar != null">avatar = #{avatar},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="location != null">location = #{location},</if>
<if test="delTime != null">del_time = #{delTime},</if>
<if test="type != null">type = #{type},</if>
upd_time = now()
</set>
where id = #{id}

View File

@ -18,10 +18,18 @@ export const checkApi = (): Promise<R<any>> => {
return rq.get<R<any>>('/check', {})
}
export const userListApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/user/list', { params })
}
export const userinfoApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/user/info', { params })
}
export const userinfoAdminApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/user/info/admin', { params })
}
export const userUpdApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/upd', data)
}
@ -30,6 +38,14 @@ export const userUpdPwdApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/upd/pwd', data)
}
export const userUpdAdminApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/upd/admin', data)
}
export const userAddApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/add', data)
}
export const userDisabledApi = (data?: object): Promise<R<any>> => {
return rq.post<any>('/user/disabled', data)
}

View File

@ -40,13 +40,21 @@ export const userParamListApi = (): Promise<R<any>> => {
}
/**
*
*
* @returns
*/
export const userParamUpdApi = (data: object): Promise<R<any>> => {
return rq.post<R<any>>('/user/param/upd', data)
}
/**
*
* @returns
*/
export const userParamUpdAdminApi = (data: object): Promise<R<any>> => {
return rq.post<R<any>>('/user/param/upd/admin', data)
}
/**
*
* @param data form
@ -304,6 +312,15 @@ export const articleWordsApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/article/stat/words', { params })
}
/**
*
* @param params
* @returns
*/
export const articleWordsUserApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/article/stat/words/user', { params })
}
/**
*
* @param params
@ -543,6 +560,15 @@ export const pictureStatApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/picture/stat', { params })
}
/**
*
* @param params {id:id}
* @returns
*/
export const pictureStatUserApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/picture/stat/user', { params })
}
/**
*
* @param data

View File

@ -174,8 +174,6 @@ const copyIcon = (icon: string) => {
@include box(143px, 110px);
padding: 15px 10px 10px 10px;
font-size: 35px;
// border-top: 1px solid var(--el-border-color);
// border-left: 1px solid var(--el-border-color);
border-right: 1px solid var(--el-border-color);
border-bottom: 1px solid var(--el-border-color);
transition: 0.3s;

View File

@ -48,7 +48,7 @@ const initAuth = () => {
/**
*
*/
const DEFAULT_USER_INFO = {
export const DEFAULT_USER_INFO = {
id: '',
type: 2,
username: '暂未登录',
@ -58,6 +58,8 @@ const DEFAULT_USER_INFO = {
articleCount: 0,
articleWords: 0,
location: '',
creTime: '',
delTime: '',
osRes: {
osType: '',
bucketName: '',

View File

@ -204,7 +204,7 @@ import Notify from '@renderer/scripts/notify'
import { useDraggable } from '@renderer/scripts/draggable'
import type { shortcutFunc } from '@renderer/scripts/shortcut-register'
import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo, isArticle } from '@renderer/views/doc/doc'
import { TempTextareaKey, ArticleReference, DocsEditorStyle, parseTocAsync } from './scripts/article'
import { TempTextareaKey, ArticleReference, parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture'
import { useResize } from './scripts/editor-preview-resize'

View File

@ -21,19 +21,24 @@
<ConfigAddUser></ConfigAddUser>
</div>
</el-tab-pane>
<el-tab-pane label="实时访问流量" name="flow" v-if="userStore.isLogin && userStore.userinfo.type === 1" :lazy="true">
<SentinelResources></SentinelResources>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { useUserStore } from '@renderer/stores/user'
import ConfigUserinfo from './SettingConfigUserinfo.vue'
import ConfigUpdPwd from './SettingConfigUpdPwd.vue'
import ConfigAddUser from './SettingConfigAddUser.vue'
import ConfigClient from './SettingConfigClient.vue'
import ConfigServer from './SettingConfigServer.vue'
import ConfigBlog from './SettingConfigBlog.vue'
import { useUserStore } from '@renderer/stores/user'
import SentinelResources from '@renderer/views/statistic/SentinelResources.vue'
const userStore = useUserStore()
const curTab = ref('client')
@ -63,6 +68,7 @@ const handleChange = (name: string) => {
:deep(.el-tabs__item.is-left.is-active) {
border-right-color: #ffffff00;
font-weight: bold;
}
:deep(.el-tabs__item) {

View File

@ -1,7 +1,7 @@
<template>
<div class="config-root" v-loading="!userStore.isLogin" element-loading-spinner="none" element-loading-text="请登录后查看...">
<div class="title">博客配置</div>
<div class="desc">博客各项参数配置若无内容请点击右侧刷新<el-button @click="refreshParam" text bg>刷新</el-button></div>
<div class="desc">博客各项参数配置若无内容请点击右侧刷新<el-button @click="refreshParam" text bg>刷新参数</el-button></div>
<el-form :model="userParamForm" label-position="right" label-width="130px" style="max-width: 800px">
<el-form-item label="文章查看地址" :required="true">
@ -109,7 +109,7 @@ const getParamList = () => {
const refreshParam = () => {
userParamRefreshApi().then((_) => {
Notify.success('刷新参数成功', '刷新成功')
Notify.success('', '刷新成功')
getParamList()
userStore.getUserinfo()
})

View File

@ -1,7 +1,11 @@
<template>
<div class="config-root" v-loading="!userStore.isLogin" element-loading-spinner="none" element-loading-text="请登录后查看...">
<div class="title">服务器配置</div>
<div class="desc">服务器各项参数配置若无内容请点击右侧刷新<el-button @click="refreshParam" text bg>刷新</el-button></div>
<div class="desc" style="margin-bottom: 0">服务器各项参数配置若无内容请点击下方刷新只有管理员用户具有操作服务器配置的权限</div>
<div class="desc">
<el-button @click="refreshParam" text bg><span class="iconbl bl-refresh-line"></span>刷新参数</el-button>
<el-button @click="showUserListDialog" text bg><span class="iconbl bl-user-line"></span>用户管理</el-button>
</div>
<el-form :model="serverParamForm" label-position="right" label-width="130px" style="max-width: 800px">
<el-form-item label="文件访问地址" :required="true">
@ -100,21 +104,35 @@
</el-input>
</el-form-item>
</el-form>
<div class="server-config">
<!-- <div class="server-config">
{{ userStore.userinfo }}
</div>
</div> -->
</div>
<!-- 自定义临时访问链接 -->
<el-dialog
v-model="isShowUserListDialog"
class="bl-dialog-fixed-body"
width="710"
style="height: 70%"
:align-center="true"
:append-to-body="true"
:destroy-on-close="true"
:close-on-click-modal="true">
<UserListSetting></UserListSetting>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useServerStore } from '@renderer/stores/server'
import { KEY_BLOSSOM_OBJECT_STORAGE_DOMAIN, useUserStore } from '@renderer/stores/user'
import { paramListApi, paramUpdApi, paramRefreshApi } from '@renderer/api/blossom'
import { getDateTimeFormat, betweenDay } from '@renderer/assets/utils/util'
import Notify from '@renderer/scripts/notify'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import UserListSetting from './setting/UserListSetting.vue'
const serverStore = useServerStore()
const userStore = useUserStore()
@ -177,7 +195,7 @@ const getParamList = () => {
const refreshParam = () => {
paramRefreshApi().then((_) => {
Notify.success('刷新参数成功', '刷新成功')
Notify.success('', '刷新成功')
getParamList()
userStore.getUserinfo()
})
@ -202,6 +220,13 @@ const autuUpdBlossomOSDomain = () => {
})
}
//#region ----------------------------------------< >--------------------------------------
const isShowUserListDialog = ref(false)
const showUserListDialog = () => {
isShowUserListDialog.value = true
}
//#endregion
const reload = () => {
getParamList()
}

View File

@ -1,7 +1,7 @@
<template>
<div class="config-root" v-loading="!userStore.isLogin" element-loading-spinner="none" element-loading-text="请登录后查看...">
<div class="title">修改用户信息</div>
<div class="desc">用户的个人信息若无内容请点击右侧刷新<el-button @click="refreshUserinfo" text bg>刷新</el-button></div>
<div class="desc">用户的个人信息若无内容请点击右侧刷新<el-button @click="refreshUserinfo" text bg>刷新信息</el-button></div>
<el-form :model="userinfoForm" :rules="rules" label-position="right" label-width="130px" style="max-width: 800px" ref="UserinfoFormRef">
<el-form-item label="ID" prop="id">
<el-input v-model="userinfoForm.id" size="default" disabled>
@ -99,8 +99,7 @@ const rules = ref<FormRules<UserinfoForm>>({
id: [{ required: true, message: '请填写用户ID', trigger: 'blur' }],
username: [{ required: true, message: '请填写用户名', trigger: 'blur' }],
nickName: [{ required: true, message: '请填写昵称', trigger: 'blur' }],
remark: [{ required: true, message: '请填写备注', trigger: 'blur' }],
avatar: [{ required: true, message: '请填写头像', trigger: 'blur' }]
remark: [{ required: true, message: '请填写备注', trigger: 'blur' }]
})
const getUserinfo = () => {
@ -112,7 +111,7 @@ const getUserinfo = () => {
const refreshUserinfo = () => {
userinfoApi().then((resp) => {
userinfoForm.value = resp.data
Notify.success('刷新用户信息成功', '刷新成功')
Notify.success('', '刷新成功')
})
}
@ -121,7 +120,7 @@ const save = async (formEl: FormInstance | undefined) => {
await formEl.validate((valid, _fields) => {
if (valid) {
userUpdApi(userinfoForm.value).then((_resp) => {
Notify.success('个人信息修改成功')
Notify.success('您的个人信息已变更', '修改成功')
userStore.checkToken(
() => {},
() => {}

View File

@ -14,9 +14,6 @@
<el-tab-pane label="关于" name="about">
<SettingAboutVue></SettingAboutVue>
</el-tab-pane>
<el-tab-pane label="访问流量" :lazy="true" name="flow">
<SentinelResources></SentinelResources>
</el-tab-pane>
</el-tabs>
</div>
<div class="version">
@ -32,7 +29,6 @@ import { isNotBlank } from '@renderer/assets/utils/obj'
import SettingLogin from './SettingLogin.vue'
import SettingConfig from './SettingConfig.vue'
import SettingAboutVue from './SettingAbout.vue'
import SentinelResources from '@renderer/views/statistic/SentinelResources.vue'
import CONFIG from '@renderer/assets/constants/system'
const userStore = useUserStore()
@ -40,7 +36,6 @@ const route = useRoute()
onMounted(() => {
let actTab = route.query.activeTab as string
console.log(actTab)
if (isNotBlank(actTab)) {
activeTab.value = actTab
} else {
@ -70,7 +65,11 @@ const getServerVersion = () => {
height: 100%;
:deep(.el-tabs__nav-wrap::after) {
background-color: var(--el-color-primary-light-8);
background-color: transparent;
}
:deep(el-tabs__nav-scroll::after) {
background-color: transparent;
}
:deep(.el-tabs__content) {

View File

@ -0,0 +1,249 @@
<template>
<div class="userinfo-setting-root">
<div class="info-title">
<div class="info" style="width: 350px; font-size: 17px">{{ curUser?.nickName }}</div>
</div>
<div class="content">
<div :class="curUser.delTime !== '0' ? 'warn-bg-zebra' : ''">
<bl-row class="info-group">
<bl-col width="80px">
<img v-if="curUser.avatar != ''" class="avatar" :src="curUser.avatar" />
<img v-else class="avatar" src="@renderer/assets/imgs/default_user_avatar.jpg" />
</bl-col>
<bl-col width="300px" height="100px" just="space-around" align="flex-start" style="padding-left: 20px">
<div class="info">ID {{ curUser.id }}</div>
<div class="info">昵称 {{ curUser.nickName }}</div>
<div class="info">用户名 {{ curUser.username }}</div>
<bl-row just="space-between">
<div>创建于 {{ curUser.creTime }}</div>
<div :class="['type', curUser.type === 1 ? 'admin' : curUser.type === 2 ? 'normal' : 'read']">
<span v-if="curUser.type === 1" class="iconbl bl-a-Securitypermissions-line"></span
>{{ curUser.type === 1 ? '管理员' : curUser.type === 2 ? '普通' : '只读' }}
</div>
</bl-row>
</bl-col>
</bl-row>
<bl-row class="info-group stat-group" just="space-around">
<div class="iconbl bl-a-texteditorhighlightcolor-line"></div>
<bl-col align="flex-end" width="30%">
<div style="margin-bottom: 5px">{{ formartNumber(articleStat.articleCount) }} 篇文章</div>
<div>{{ formartNumber(articleStat.articleWords) }} </div>
</bl-col>
<div class="iconbl bl-image--line"></div>
<bl-col align="flex-end" width="30%">
<div style="margin-bottom: 5px">{{ formartNumber(pictureStat.pictureCount) }} 个文件</div>
<div>{{ formatFileSize(pictureStat.pictureSize) }}</div>
</bl-col>
</bl-row>
</div>
<el-divider style="margin: 0"></el-divider>
<div class="info-group">
<div class="row">用户类型</div>
<el-select class="row" size="default" v-model="curUser.type" style="width: 100%">
<el-option :value="1" label="管理员">
<span style="float: left">管理员</span>
<span style="float: right; color: var(--bl-text-color-light)">允许管理用户修改服务器配置</span>
</el-option>
<el-option :value="2" label="普通用户">
<span style="float: left">普通用户</span>
<span style="float: right; color: var(--bl-text-color-light)">除管理员特权外的所有功能</span>
</el-option>
<el-option :value="3" label="只读用户">
<span style="float: left">只读用户</span>
<span style="float: right; color: var(--bl-text-color-light)">只有查看权限无法修改与删除</span>
</el-option>
</el-select>
<div class="row">和风天气地区</div>
<el-input class="row" size="default" v-model="curUser.location">
<template #append>
<el-button @click="openExtenal('https://github.com/qwd/LocationList/blob/master/China-City-List-latest.csv')">查看城市代码</el-button>
</template>
</el-input>
<div class="row">博客地址</div>
<el-input type="textarea" class="row" size="default" :row="2" resize="none" v-model="curUser.userParams.WEB_ARTICLE_URL">
<template #prefix>
<div class="iconbl bl-blog"></div>
</template>
</el-input>
</div>
<el-divider style="margin: 0"></el-divider>
<bl-row just="space-between" class="info-group">
<el-button-group>
<el-button size="default" text bg type="danger">删除</el-button>
<el-button size="default" text bg type="warning" @click="disabled">
{{ curUser.delTime === '0' ? '禁用' : '启用' }}
</el-button>
</el-button-group>
<el-button size="default" type="primary" @click="upd">保存</el-button>
</bl-row>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, onMounted, ref } from 'vue'
import { userinfoAdminApi, userDisabledApi, userUpdAdminApi } from '@renderer/api/auth'
import { articleWordsUserApi, pictureStatUserApi, userParamUpdAdminApi } from '@renderer/api/blossom'
import { DEFAULT_USER_INFO } from '@renderer/stores/user'
import type { Userinfo } from '@renderer/stores/user'
import { openExtenal } from '@renderer/assets/utils/electron'
import { formartNumber, formatFileSize } from '@renderer/assets/utils/util'
import { ElMessage, ElMessageBox } from 'element-plus'
import { emit } from 'process'
onMounted(() => {
getUserInfo()
})
//#region ----------------------------------------< >--------------------------------------
const props = defineProps({
id: { type: String, default: '' }
})
const articleStat = ref({ articleCount: 0, articleWords: 0 })
const pictureStat = ref({ pictureCount: 0, pictureSize: 0 })
const curUser = ref<Userinfo>(DEFAULT_USER_INFO)
const getUserInfo = () => {
userinfoAdminApi({ id: props.id }).then((resp) => {
curUser.value = resp.data
console.log(curUser.value)
})
articleWordsUserApi({ id: props.id }).then((resp) => {
articleStat.value = resp.data
})
pictureStatUserApi({ id: props.id }).then((resp) => {
pictureStat.value = resp.data
})
}
/**
* 禁用启用用户
*/
const disabled = () => {
if (curUser.value.delTime === '0') {
ElMessageBox.confirm(
`禁用后该用户将无法再次登录, 是否禁用?<br/>
<div style="width:100%;text-align:center;font-size:14px">${curUser.value.nickName} (${curUser.value.id})</div>`,
{
confirmButtonText: '确定禁用',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
draggable: true
}
).then(() => {
userDisabledApi({ id: props.id, delTime: Date.now() }).then((_resp) => {
emits('saved')
})
})
} else {
userDisabledApi({ id: props.id, delTime: 0 }).then((_resp) => {
emits('saved')
})
}
}
/**
* 修改用户信息
*/
const upd = () => {
ElMessageBox.confirm(
`修改后各项配置会在用户重新登录后生效, 是否保存用户信息?<br/>
<div style="width:100%;text-align:center;font-size:14px">${curUser.value.nickName} (${curUser.value.id})</div>`,
{
confirmButtonText: '确定保存',
cancelButtonText: '取消',
type: 'info',
dangerouslyUseHTMLString: true,
draggable: true
}
).then(() => {
userUpdAdminApi({
id: props.id,
type: curUser.value.type,
location: curUser.value.location
}).then((_resp) => {
userParamUpdAdminApi({
userId: props.id,
paramName: 'WEB_ARTICLE_URL',
paramValue: curUser.value.userParams.WEB_ARTICLE_URL
}).then((_resp) => {
emits('saved')
ElMessage.info('用户信息已更新')
})
})
})
}
const emits = defineEmits(['saved'])
//#endregion
</script>
<style lang="scss" scoped>
@import '@renderer/assets/styles/bl-dialog-info';
@import '@renderer/views/index/styles/user-setting';
.userinfo-setting-root {
@include box(100%, 100%);
@include font(13px, 300);
.info {
@include ellipsis();
width: 100%;
}
.label {
margin-top: 10px;
}
.content {
@include box(100%, calc(100% - 50px));
position: relative;
.info-group {
padding: 10px;
.row {
margin-bottom: 10px;
}
}
.stat-group {
font-size: 12px;
.bl-a-texteditorhighlightcolor-line,
.bl-image--line {
@include themeText(2px 4px 5px rgba(134, 134, 134, 0.3), 2px 4px 7px #222222);
@include themeColor(#8a8a8a, #656565);
position: absolute;
font-size: 30px;
}
.bl-a-texteditorhighlightcolor-line {
left: 30px;
}
.bl-image--line {
left: 230px;
}
}
.avatar {
@include box(80px, 80px);
@include themeBrightness();
@include themeShadow(2px 4px 7px 2px rgba(134, 134, 134, 0.3), 2px 4px 7px 2px #000000);
@include themeBorder(2px, #a8abb2, #707070);
object-fit: cover;
border-radius: 6px;
margin-left: 10px;
}
}
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<div class="userlist-setting-root">
<div class="info-title">
<div class="iconbl bl-user-line"></div>
用户管理
</div>
<div class="content">
<bl-row class="stat"
>
<div>{{ users.length }}</div>
名用户管理员
<div class="admin">{{ userStatComputed.admin }}</div>
普通用户
<div class="normal">{{ userStatComputed.normal }}</div>
只读用户
<div class="read">{{ userStatComputed.read }}</div>
</bl-row
>
<bl-row class="search" just="space-between">
<div>
<el-input size="default" placeholder="搜索用户名或昵称" style="width: 200px" v-model="userSearch"></el-input>
<el-select size="default" placeholder="用户名类型" style="width: 120px; margin-left: 10px" v-model="userTypeSearch" clearable>
<el-option :value="1" label="管理员" />
<el-option :value="2" label="普通用户" />
<el-option :value="3" label="只读用户" />
</el-select>
</div>
<el-button size="default" class="iconbl bl-refresh-line" @click="getUserList"></el-button>
</bl-row>
<div class="user-container">
<div :class="['user-item', user.delTime !== '0' ? 'warn-bg-zebra' : '']" v-for="user in usersComputed" @click="showDetailDialog(user.id)">
<img v-if="user.avatar != ''" class="avatar" :src="user.avatar" />
<img v-else class="avatar" src="@renderer/assets/imgs/default_user_avatar.jpg" />
<div class="nickname">{{ user.nickName }}</div>
<div class="username">{{ user.username }}</div>
<bl-row class="time" just="space-between">{{ user.creTime }}</bl-row>
<div class="id">{{ user.id }}</div>
<div :class="['type', user.type === 1 ? 'admin' : user.type === 2 ? 'normal' : 'read']">
<span v-if="user.type === 1" class="iconbl bl-a-Securitypermissions-line"></span
>{{ user.type === 1 ? '管理员' : user.type === 2 ? '普通' : '只读' }}
</div>
</div>
</div>
</div>
</div>
<el-dialog
v-model="isShowDetailDialog"
class="bl-dialog-fixed-body"
width="400"
style="height: 535px"
:align-center="true"
:append-to-body="true"
:destroy-on-close="true"
:close-on-click-modal="true">
<UserInfoSetting :id="curUserId" @saved="infoSaved"></UserInfoSetting>
</el-dialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { userListApi } from '@renderer/api/auth'
import { isNotNull, isNull } from '@renderer/assets/utils/obj'
import { computed } from 'vue'
import UserInfoSetting from './UserInfoSetting.vue'
onMounted(() => {
getUserList()
})
//#region ----------------------------------------< >--------------------------------------
type UserList = {
id: string
avatar: string
nickName: string
username: string
creTime: string
delTime: string
type: number
}
const userSearch = ref()
const userTypeSearch = ref()
const users = ref<UserList[]>([])
/**
* 用户搜索
*/
const usersComputed = computed(() => {
return users.value
.filter((user: UserList) => {
if (isNull(userSearch.value) && isNull(userTypeSearch.value)) {
return true
}
let isName: boolean = true
if (isNotNull(userSearch.value)) {
isName =
user.username.toLowerCase().includes(userSearch.value.toLowerCase()) || user.nickName.toLowerCase().includes(userSearch.value.toLowerCase())
}
let isType: boolean = true
if (isNotNull(userTypeSearch.value)) {
isType = user.type === userTypeSearch.value
}
if (isName && isType) {
return true
}
return false
})
.reverse()
})
/**
* 统计用户数
*/
const userStatComputed = computed(() => {
let stat = {
admin: 0,
normal: 0,
read: 0
}
for (let i = 0; i < users.value.length; i++) {
const u = users.value[i]
if (u.type === 1) {
stat.admin++
}
if (u.type === 2) {
stat.normal++
}
if (u.type === 3) {
stat.read++
}
}
return stat
})
const getUserList = () => {
userListApi().then((resp) => {
users.value = resp.data
})
}
//#endregion
//#region ----------------------------------------< >--------------------------------------
const isShowDetailDialog = ref(false)
const curUserId = ref<string>()
const showDetailDialog = (id: string) => {
isShowDetailDialog.value = true
curUserId.value = id
}
const infoSaved = () => {
getUserList()
isShowDetailDialog.value = false
}
//#endregion
</script>
<style lang="scss" scoped>
@import '@renderer/assets/styles/bl-dialog-info';
@import '@renderer/views/index/styles/user-setting';
.userlist-setting-root {
@include box(100%, 100%);
.content {
@include box(100%, calc(100% - 50px));
padding: 10px 10px 20px;
.stat {
height: 25px;
font-size: 13px;
div {
padding: 0 3px;
margin: 0 2px;
font-size: 13px;
transition: all 0.3s;
border-radius: 4px;
cursor: pointer;
}
.admin:hover {
background-color: $admin-bg;
color: $admin-color;
}
.normal:hover {
background-color: $normal-bg;
color: $normal-color;
}
.read:hover {
background-color: $read-bg;
color: $read-color;
}
}
.search {
height: 30px;
margin: 10px 0 10px 0;
}
.user-container {
@include box(100%, calc(100% - 75px));
@include flex(row, flex-start, flex-start);
align-content: flex-start;
flex-wrap: wrap;
overflow-y: scroll;
padding: 10px 5px 10px 10px;
.user-item {
@include font(13px, 300);
@include themeBg(#f5f5f5, #252525);
width: 150px;
margin-bottom: 25px;
margin-right: 15px;
padding: 8px;
border-radius: 4px;
transition:
box-shadow 0.2s,
transform 0.3s;
position: relative;
cursor: pointer;
&:hover {
@include themeShadow(0 3px 5px 0 rgb(190, 190, 190), 0 3px 5px 0 rgb(20, 20, 20));
transform: translateY(-5px);
.download-btn {
opacity: 1;
}
}
.avatar {
@include box(50px, 50px);
@include themeBrightness();
object-fit: cover;
border-radius: 6px;
margin-bottom: 10px;
}
.nickname {
@include font(15px, 300);
@include ellipsis();
}
.username {
@include font(12px, 300);
@include ellipsis();
color: var(--bl-text-color-light);
}
.time {
@include font(11px, 300);
color: var(--bl-text-color-light);
}
.id {
@include font(11px, 300);
color: var(--bl-text-color-light);
position: absolute;
top: 35px;
right: 10px;
}
.type {
position: absolute;
top: 10px;
right: 10px;
}
}
}
}
}
</style>

View File

@ -45,6 +45,11 @@
line-height: 30px;
margin-bottom: 30px;
color: var(--bl-text-color-light);
.iconbl {
@include font(12px, 300);
padding-right: 5px;
}
}
code {

View File

@ -0,0 +1,66 @@
$admin-bg: #ca9100;
$admin-color: var(--bl-html-color);
$normal-bg: var(--el-color-primary);
$normal-color: var(--bl-html-color);
$read-bg: var(--bl-bg-color);
$read-color: #939393;
.bg-zebra {
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: 20px 20px;
z-index: 0;
}
}
.warn-bg-zebra {
@extend .bg-zebra;
&:before {
background-image: linear-gradient(
-45deg,
#ca910021 0%,
#ca910021 25%,
transparent 25%,
transparent 50%,
#ca910021 50%,
#ca910021 75%,
transparent 75%,
transparent 100%
);
}
}
.type {
@include font(11px, 500);
@include themeShadow(0 2px 5px 0 rgb(190, 190, 190), 0 2px 3px 0 rgb(20, 20, 20));
border-radius: 4px;
padding: 2px 5px;
margin-left: 10px;
.iconbl {
font-size: 12px;
padding-right: 2px;
}
}
.type.admin {
background-color: $admin-bg;
color: $admin-color;
}
.type.normal {
background-color: $normal-bg;
color: $normal-color;
}
.type.read {
background-color: $read-bg;
color: $read-color;
}

View File

@ -4,14 +4,13 @@
<div class="table-operator">
<bl-row class="component-title">资源列表</bl-row>
<bl-row style="margin-top: 10px">
<!-- 资源搜索框 -->
<el-input v-model="dataSearch" placeholder="搜索资源名" />
<el-button type="primary" :icon="Refresh" @click="getResource()" style="margin-left: 10px">刷新</el-button>
<el-button type="primary" :icon="Refresh" @click="getResource()" style="margin-left: 10px"></el-button>
</bl-row>
</div>
<div class="table">
<el-table border height="100%" tooltip-effect="dark" v-loading="tableLoading" :data="filterTableDatas" @row-click="rowClick">
<el-table-column sortable prop="resource" min-width="250" :show-overflow-tooltip="true" label="资源名称(24小时内)" />
<el-table-column sortable prop="resource" min-width="170" :show-overflow-tooltip="true" label="资源名称(24小时内)" />
<el-table-column sortable prop="success" width="80" align="right" label="请求数" />
<el-table-column sortable prop="avgRt" width="90" align="right" label="avgRT">
<template #default="scope">
@ -35,14 +34,14 @@
<div class="chart-line">
<bl-row class="component-title" just="space-between">
<div>
资源折线图
<div class="title">
流量折线图
<span style="font-size: 12px"
>查询最近{{ lineSearchParam.interval }}的流量信息按每{{ lineSearchParam.customInterval }}分钟聚合资源:
{{ lineSearchParam.resource }}</span
>
</div>
<el-button-group>
<el-button-group style="width: 195px">
<el-button round @click="intervalClick('5m', 1)">5M</el-button>
<el-button round @click="intervalClick('10m', 1)">10M</el-button>
<el-button round @click="intervalClick('1h', 1)">1H</el-button>
@ -127,16 +126,23 @@ const rowClick = (row: ResourceRow) => {
padding: 0 30px 30px 0;
.component-title {
width: 100%;
color: var(--bl-text-color);
border: 1px solid var(--el-border-color);
background-color: var(--bl-bg-color);
border-radius: 4px;
padding: 5px 10px;
height: 36px;
.title {
width: calc(100% - 195px);
overflow: hidden;
@include ellipsis();
}
}
.table-container {
@include box(530px, 100%);
@include box(420px, 100%);
.table-operator {
@include box(100%, 80px);
@ -163,7 +169,7 @@ const rowClick = (row: ResourceRow) => {
}
.chart-line {
@include box(calc(100% - 545px), 100%);
@include box(calc(100% - 420px), 100%);
max-height: 500px;
margin-left: 15px;
}