[feat]支持假删除以及数据恢复

This commit is contained in:
大石头 2024-07-25 17:33:39 +08:00
parent ee3324af21
commit 6cf4222e9b
13 changed files with 379 additions and 253 deletions

View File

@ -55,7 +55,7 @@ public class OAuthController : ControllerBase
var (jwt, ex) = _tokenService.DecodeTokenWithError(model.refresh_token, set.JwtSecret);
// 验证应用
var app = App.FindByName(jwt?.Subject);
var app = _tokenService.FindByName(jwt?.Subject);
if (app == null || !app.Enable)
ex ??= new ApiException(403, $"无效应用[{jwt.Subject}]");

View File

@ -754,6 +754,17 @@
<td>是否抓取头像并保存到本地</td>
</tr>
<tr>
<td>IsDeleted</td>
<td>删除</td>
<td>Boolean</td>
<td></td>
<td></td>
<td></td>
<td>N</td>
<td>是否已删除,可恢复</td>
</tr>
<tr>
<td>CreateUserID</td>
<td>创建者</td>
@ -1713,6 +1724,17 @@
<td></td>
</tr>
<tr>
<td>IsDeleted</td>
<td>删除</td>
<td>Boolean</td>
<td></td>
<td></td>
<td></td>
<td>N</td>
<td>是否已删除,可恢复</td>
</tr>
<tr>
<td>CreateUserID</td>
<td>创建者</td>

View File

@ -122,6 +122,7 @@
<Column Name="SecurityKey" DataType="String" Length="500" Description="安全密钥。公钥用于RSA加密用户密码在通信链路上保护用户密码安全密钥前面可以增加keyName形成keyName$keyValue用于向服务端指示所使用的密钥标识方便未来更换密钥。" />
<Column Name="FieldMap" DataType="String" Length="500" Description="字段映射。SSO用户字段如何映射到OAuthClient内部属性" />
<Column Name="FetchAvatar" DataType="Boolean" Description="抓取头像。是否抓取头像并保存到本地" />
<Column Name="IsDeleted" DataType="Boolean" Description="删除。是否已删除,可恢复" />
<Column Name="CreateUserID" DataType="Int32" Description="创建者" Category="扩展" />
<Column Name="CreateTime" DataType="DateTime" Description="创建时间" Category="扩展" />
<Column Name="CreateIP" DataType="String" Description="创建地址" Category="扩展" />
@ -234,6 +235,7 @@
<Column Name="Expired" DataType="DateTime" Description="过期时间。空表示永不过期" />
<Column Name="Auths" DataType="Int32" Description="次数" />
<Column Name="LastAuth" DataType="DateTime" Description="最后请求" />
<Column Name="IsDeleted" DataType="Boolean" Description="删除。是否已删除,可恢复" />
<Column Name="CreateUserID" DataType="Int32" Description="创建者" Category="扩展" />
<Column Name="CreateTime" DataType="DateTime" Description="创建时间" Category="扩展" />
<Column Name="CreateIP" DataType="String" Description="创建地址" Category="扩展" />

View File

@ -1,234 +1,229 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.ComponentModel;
using NewLife.Cube.Web.Models;
using NewLife.Data;
using NewLife.Log;
using NewLife.Security;
using NewLife.Serialization;
using XCode;
using XCode.Membership;
namespace NewLife.Cube.Entity
namespace NewLife.Cube.Entity;
/// <summary>OAuth2.0授权类型</summary>
public enum GrantTypes
{
/// <summary>OAuth2.0授权类型</summary>
public enum GrantTypes
/// <summary>
/// 授权码
/// </summary>
AuthorizationCode = 0,
/// <summary>
/// 隐藏式
/// </summary>
Implicit,
/// <summary>
/// 密码式
/// </summary>
Password,
/// <summary>
/// 客户端凭证
/// </summary>
ClientCredentials,
}
/// <summary>OAuth配置。需要连接的OAuth认证方</summary>
public partial class OAuthConfig : Entity<OAuthConfig>
{
#region
static OAuthConfig()
{
/// <summary>
/// 授权码
/// </summary>
AuthorizationCode = 0,
// 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
//var df = Meta.Factory.AdditionalFields;
//df.Add(nameof(CreateUserID));
/// <summary>
/// 隐藏式
/// </summary>
Implicit,
// 过滤器 UserModule、TimeModule、IPModule
Meta.Modules.Add<UserModule>();
Meta.Modules.Add<TimeModule>();
Meta.Modules.Add<IPModule>();
/// <summary>
/// 密码式
/// </summary>
Password,
/// <summary>
/// 客户端凭证
/// </summary>
ClientCredentials,
// 单对象缓存
var sc = Meta.SingleCache;
sc.FindSlaveKeyMethod = k => Find(_.Name == k);
sc.GetSlaveKeyMethod = e => e.Name;
}
/// <summary>OAuth配置。需要连接的OAuth认证方</summary>
public partial class OAuthConfig : Entity<OAuthConfig>
/// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
/// <param name="isNew">是否插入</param>
public override void Valid(Boolean isNew)
{
#region
static OAuthConfig()
// 如果没有脏数据,则不需要进行任何处理
if (!HasDirty) return;
// 这里验证参数范围,建议抛出参数异常,指定参数名,前端用户界面可以捕获参数异常并聚焦到对应的参数输入框
if (Name.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Name), "名称不能为空!");
// 建议先调用基类方法,基类方法会做一些统一处理
base.Valid(isNew);
// 不要写AuthUrl默认地址否则会影响微信登录
if (Name.EqualIgnoreCase("NewLife"))
{
// 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
//var df = Meta.Factory.AdditionalFields;
//df.Add(nameof(CreateUserID));
// 过滤器 UserModule、TimeModule、IPModule
Meta.Modules.Add<UserModule>();
Meta.Modules.Add<TimeModule>();
Meta.Modules.Add<IPModule>();
// 单对象缓存
var sc = Meta.SingleCache;
sc.FindSlaveKeyMethod = k => Find(_.Name == k);
sc.GetSlaveKeyMethod = e => e.Name;
if (AuthUrl.IsNullOrEmpty()) AuthUrl = "authorize?response_type={response_type}&client_id={key}&redirect_uri={redirect}&state={state}&scope={scope}";
if (AccessUrl.IsNullOrEmpty()) AccessUrl = "access_token?grant_type=authorization_code&client_id={key}&client_secret={secret}&code={code}&state={state}&redirect_uri={redirect}";
}
/// <summary>验证并修补数据,通过抛出异常的方式提示验证失败。</summary>
/// <param name="isNew">是否插入</param>
public override void Valid(Boolean isNew)
{
// 如果没有脏数据,则不需要进行任何处理
if (!HasDirty) return;
// 这里验证参数范围,建议抛出参数异常,指定参数名,前端用户界面可以捕获参数异常并聚焦到对应的参数输入框
if (Name.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Name), "名称不能为空!");
// 建议先调用基类方法,基类方法会做一些统一处理
base.Valid(isNew);
// 不要写AuthUrl默认地址否则会影响微信登录
if (Name.EqualIgnoreCase("NewLife"))
{
if (AuthUrl.IsNullOrEmpty()) AuthUrl = "authorize?response_type={response_type}&client_id={key}&redirect_uri={redirect}&state={state}&scope={scope}";
if (AccessUrl.IsNullOrEmpty()) AccessUrl = "access_token?grant_type=authorization_code&client_id={key}&client_secret={secret}&code={code}&state={state}&redirect_uri={redirect}";
}
if (FieldMap.IsNullOrEmpty())
FieldMap = new OAuthFieldMap().ToJson(true);
else
FieldMap = FieldMap.ToJsonEntity<OAuthFieldMap>().ToJson(true);
}
/// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected override void InitData()
{
// InitData一般用于当数据表没有数据时添加一些默认数据该实体类的任何第一次数据库操作都会触发该方法默认异步调用
if (Meta.Session.Count > 0) return;
if (XTrace.Debug) XTrace.WriteLine("开始初始化OAuthConfig[OAuth配置]数据……");
//Add("NewLife", "新生命用户中心", "/Content/images/logo/NewLife.png");
var entity = new OAuthConfig
{
Name = "NewLife",
NickName = "新生命用户中心",
Logo = "/Content/images/logo/NewLife.png",
Server = "https://sso.newlifex.com/sso",
AppId = "NewLife.Cube",
Secret = Rand.NextString(16),
Enable = true,
Debug = true,
Visible = true,
AutoRegister = true,
};
entity.Insert();
Add("QQ", "QQ", "/Content/images/logo/QQ.png");
Add("Github", "Github", "/Content/images/logo/Github.png");
Add("Baidu", "百度", "/Content/images/logo/Baidu.png");
Add("Ding", "钉钉", "/Content/images/logo/Ding.png", "snsapi_qrlogin扫码登录snsapi_auth钉钉内免登snsapi_login密码登录");
Add("QyWeiXin", "企业微信", "/Content/images/logo/QyWeiXin.png");
//Add("Weixin", "微信公众号", "/Content/images/logo/Weixin.png", "snsapi_base静默登录snsapi_userinfo需要用户关注后授权");
var cfg = new OAuthConfig
{
Name = "Weixin",
NickName = "微信公众号",
Logo = "/Content/images/logo/Weixin.png",
Remark = "snsapi_base静默登录snsapi_userinfo需要用户关注后授权",
Visible = false,
AutoRegister = true,
};
cfg.Insert();
Add("OpenWeixin", "微信开放平台", "/Content/images/logo/Weixin.png", "snsapi_login用于扫码登录");
Add("Microsoft", "微软", "/Content/images/logo/Microsoft.png");
//Add("Weibo", "微博", "/Content/images/logo/Weibo.png");
//Add("Taobao", "淘宝", "/Content/images/logo/Taobao.png");
//Add("Alipay", "支付宝", "/Content/images/logo/Alipay.png");
if (XTrace.Debug) XTrace.WriteLine("完成初始化OAuthConfig[OAuth配置]数据!");
}
/// <summary>已重载。显示友好名称</summary>
/// <returns></returns>
public override String ToString() => !NickName.IsNullOrEmpty() ? NickName : Name;
#endregion
#region
#endregion
#region
/// <summary>根据编号查找</summary>
/// <param name="id">编号</param>
/// <returns>实体对象</returns>
public static OAuthConfig FindByID(Int32 id)
{
if (id <= 0) return null;
// 实体缓存
if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.ID == id);
// 单对象缓存
return Meta.SingleCache[id];
//return Find(_.ID == id);
}
/// <summary>根据名称查找</summary>
/// <param name="name">名称</param>
/// <returns>实体对象</returns>
public static OAuthConfig FindByName(String name)
{
// 实体缓存
if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Name.EqualIgnoreCase(name));
// 单对象缓存
//return Meta.SingleCache.GetItemWithSlaveKey(name) as OAuthConfig;
return Find(_.Name == name);
}
#endregion
#region
/// <summary>高级查询</summary>
/// <param name="name">名称。AppID</param>
/// <param name="start">更新时间开始</param>
/// <param name="end">更新时间结束</param>
/// <param name="key">关键字</param>
/// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
/// <returns>实体列表</returns>
public static IList<OAuthConfig> Search(String name, DateTime start, DateTime end, String key, PageParameter page)
{
var exp = new WhereExpression();
if (!name.IsNullOrEmpty()) exp &= _.Name == name;
exp &= _.UpdateTime.Between(start, end);
if (!key.IsNullOrEmpty()) exp &= _.Server.Contains(key) | _.AccessServer.Contains(key) | _.AppId.Contains(key) | _.Secret.Contains(key) | _.Scope.Contains(key) | _.AppUrl.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key) | _.Remark.Contains(key);
return FindAll(exp, page);
}
#endregion
#region
/// <summary>添加配置</summary>
/// <param name="name"></param>
/// <param name="nickName"></param>
/// <param name="logo"></param>
/// <param name="remark"></param>
/// <returns></returns>
public static OAuthConfig Add(String name, String nickName, String logo, String remark = null)
{
var entity = new OAuthConfig
{
Name = name,
NickName = nickName,
Logo = logo,
Visible = true,
AutoRegister = true,
Remark = remark,
};
entity.Insert();
return entity;
}
/// <summary>获取全部有效设置</summary>
/// <param name="grantType">授权类型</param>
/// <returns></returns>
public static IList<OAuthConfig> GetValids(GrantTypes grantType) => FindAllWithCache().Where(e => e.Enable && e.GrantType == grantType).OrderByDescending(e => e.Sort).ThenByDescending(e => e.ID).ToList();
/// <summary>获取全部有效且可见设置</summary>
/// <returns></returns>
public static IList<OAuthConfig> GetVisibles() => FindAllWithCache().Where(e => e.Enable && e.Visible).OrderByDescending(e => e.Sort).ThenByDescending(e => e.ID).ToList();
#endregion
if (FieldMap.IsNullOrEmpty())
FieldMap = new OAuthFieldMap().ToJson(true);
else
FieldMap = FieldMap.ToJsonEntity<OAuthFieldMap>().ToJson(true);
}
/// <summary>首次连接数据库时初始化数据,仅用于实体类重载,用户不应该调用该方法</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected override void InitData()
{
// InitData一般用于当数据表没有数据时添加一些默认数据该实体类的任何第一次数据库操作都会触发该方法默认异步调用
if (Meta.Session.Count > 0) return;
if (XTrace.Debug) XTrace.WriteLine("开始初始化OAuthConfig[OAuth配置]数据……");
//Add("NewLife", "新生命用户中心", "/Content/images/logo/NewLife.png");
var entity = new OAuthConfig
{
Name = "NewLife",
NickName = "新生命用户中心",
Logo = "/Content/images/logo/NewLife.png",
Server = "https://sso.newlifex.com/sso",
AppId = "NewLife.Cube",
Secret = Rand.NextString(16),
Enable = true,
Debug = true,
Visible = true,
AutoRegister = true,
};
entity.Insert();
Add("QQ", "QQ", "/Content/images/logo/QQ.png");
Add("Github", "Github", "/Content/images/logo/Github.png");
Add("Baidu", "百度", "/Content/images/logo/Baidu.png");
Add("Ding", "钉钉", "/Content/images/logo/Ding.png", "snsapi_qrlogin扫码登录snsapi_auth钉钉内免登snsapi_login密码登录");
Add("QyWeiXin", "企业微信", "/Content/images/logo/QyWeiXin.png");
//Add("Weixin", "微信公众号", "/Content/images/logo/Weixin.png", "snsapi_base静默登录snsapi_userinfo需要用户关注后授权");
var cfg = new OAuthConfig
{
Name = "Weixin",
NickName = "微信公众号",
Logo = "/Content/images/logo/Weixin.png",
Remark = "snsapi_base静默登录snsapi_userinfo需要用户关注后授权",
Visible = false,
AutoRegister = true,
};
cfg.Insert();
Add("OpenWeixin", "微信开放平台", "/Content/images/logo/Weixin.png", "snsapi_login用于扫码登录");
Add("Microsoft", "微软", "/Content/images/logo/Microsoft.png");
//Add("Weibo", "微博", "/Content/images/logo/Weibo.png");
//Add("Taobao", "淘宝", "/Content/images/logo/Taobao.png");
//Add("Alipay", "支付宝", "/Content/images/logo/Alipay.png");
if (XTrace.Debug) XTrace.WriteLine("完成初始化OAuthConfig[OAuth配置]数据!");
}
/// <summary>已重载。显示友好名称</summary>
/// <returns></returns>
public override String ToString() => !NickName.IsNullOrEmpty() ? NickName : Name;
#endregion
#region
#endregion
#region
/// <summary>根据编号查找</summary>
/// <param name="id">编号</param>
/// <returns>实体对象</returns>
public static OAuthConfig FindByID(Int32 id)
{
if (id <= 0) return null;
// 实体缓存
if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.ID == id);
// 单对象缓存
return Meta.SingleCache[id];
//return Find(_.ID == id);
}
/// <summary>根据名称查找</summary>
/// <param name="name">名称</param>
/// <returns>实体对象</returns>
public static OAuthConfig FindByName(String name)
{
// 实体缓存
if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.Name.EqualIgnoreCase(name));
// 单对象缓存
//return Meta.SingleCache.GetItemWithSlaveKey(name) as OAuthConfig;
return Find(_.Name == name);
}
#endregion
#region
/// <summary>高级查询</summary>
/// <param name="name">名称。AppID</param>
/// <param name="start">更新时间开始</param>
/// <param name="end">更新时间结束</param>
/// <param name="key">关键字</param>
/// <param name="page">分页参数信息。可携带统计和数据权限扩展查询等信息</param>
/// <returns>实体列表</returns>
public static IList<OAuthConfig> Search(String name, DateTime start, DateTime end, String key, PageParameter page)
{
var exp = new WhereExpression();
if (!name.IsNullOrEmpty()) exp &= _.Name == name;
exp &= _.UpdateTime.Between(start, end);
if (!key.IsNullOrEmpty()) exp &= _.Server.Contains(key) | _.AccessServer.Contains(key) | _.AppId.Contains(key) | _.Secret.Contains(key) | _.Scope.Contains(key) | _.AppUrl.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key) | _.Remark.Contains(key);
return FindAll(exp, page);
}
#endregion
#region
/// <summary>添加配置</summary>
/// <param name="name"></param>
/// <param name="nickName"></param>
/// <param name="logo"></param>
/// <param name="remark"></param>
/// <returns></returns>
public static OAuthConfig Add(String name, String nickName, String logo, String remark = null)
{
var entity = new OAuthConfig
{
Name = name,
NickName = nickName,
Logo = logo,
Visible = true,
AutoRegister = true,
Remark = remark,
};
entity.Insert();
return entity;
}
/// <summary>获取全部有效设置</summary>
/// <param name="grantType">授权类型</param>
/// <returns></returns>
public static IList<OAuthConfig> GetValids(GrantTypes grantType) => FindAllWithCache().Where(e => e.Enable && !e.IsDeleted && e.GrantType == grantType).OrderByDescending(e => e.Sort).ThenByDescending(e => e.ID).ToList();
/// <summary>获取全部有效且可见设置</summary>
/// <returns></returns>
public static IList<OAuthConfig> GetVisibles() => FindAllWithCache().Where(e => e.Enable && !e.IsDeleted && e.Visible).OrderByDescending(e => e.Sort).ThenByDescending(e => e.ID).ToList();
#endregion
}

View File

@ -206,6 +206,14 @@ public partial class OAuthConfig
[BindColumn("FetchAvatar", "抓取头像。是否抓取头像并保存到本地", "")]
public Boolean FetchAvatar { get => _FetchAvatar; set { if (OnPropertyChanging("FetchAvatar", value)) { _FetchAvatar = value; OnPropertyChanged("FetchAvatar"); } } }
private Boolean _IsDeleted;
/// <summary>删除。是否已删除,可恢复</summary>
[DisplayName("删除")]
[Description("删除。是否已删除,可恢复")]
[DataObjectField(false, false, false, 0)]
[BindColumn("IsDeleted", "删除。是否已删除,可恢复", "")]
public Boolean IsDeleted { get => _IsDeleted; set { if (OnPropertyChanging("IsDeleted", value)) { _IsDeleted = value; OnPropertyChanged("IsDeleted"); } } }
private Int32 _CreateUserID;
/// <summary>创建者</summary>
[Category("扩展")]
@ -301,6 +309,7 @@ public partial class OAuthConfig
"SecurityKey" => _SecurityKey,
"FieldMap" => _FieldMap,
"FetchAvatar" => _FetchAvatar,
"IsDeleted" => _IsDeleted,
"CreateUserID" => _CreateUserID,
"CreateTime" => _CreateTime,
"CreateIP" => _CreateIP,
@ -337,6 +346,7 @@ public partial class OAuthConfig
case "SecurityKey": _SecurityKey = Convert.ToString(value); break;
case "FieldMap": _FieldMap = Convert.ToString(value); break;
case "FetchAvatar": _FetchAvatar = value.ToBoolean(); break;
case "IsDeleted": _IsDeleted = value.ToBoolean(); break;
case "CreateUserID": _CreateUserID = value.ToInt(); break;
case "CreateTime": _CreateTime = value.ToDateTime(); break;
case "CreateIP": _CreateIP = Convert.ToString(value); break;
@ -429,6 +439,9 @@ public partial class OAuthConfig
/// <summary>抓取头像。是否抓取头像并保存到本地</summary>
public static readonly Field FetchAvatar = FindByName("FetchAvatar");
/// <summary>删除。是否已删除,可恢复</summary>
public static readonly Field IsDeleted = FindByName("IsDeleted");
/// <summary>创建者</summary>
public static readonly Field CreateUserID = FindByName("CreateUserID");
@ -525,6 +538,9 @@ public partial class OAuthConfig
/// <summary>抓取头像。是否抓取头像并保存到本地</summary>
public const String FetchAvatar = "FetchAvatar";
/// <summary>删除。是否已删除,可恢复</summary>
public const String IsDeleted = "IsDeleted";
/// <summary>创建者</summary>
public const String CreateUserID = "CreateUserID";

View File

@ -167,16 +167,16 @@ public partial class App : Entity<App>
return true;
}
/// <summary>验证应用密钥是否有效</summary>
/// <param name="appkey"></param>
/// <returns></returns>
public static App Valid(String appkey)
{
var app = FindBySecret(appkey);
if (app == null || !app.Enable) throw new XException("非法授权!");
///// <summary>验证应用密钥是否有效</summary>
///// <param name="appkey"></param>
///// <returns></returns>
//public static App Valid(String appkey)
//{
// var app = FindBySecret(appkey);
// if (app == null || !app.Enable) throw new XException("非法授权!");
return app;
}
// return app;
//}
/// <summary>写应用历史</summary>
/// <param name="action"></param>

View File

@ -168,6 +168,14 @@ public partial class App
[BindColumn("LastAuth", "最后请求", "")]
public DateTime LastAuth { get => _LastAuth; set { if (OnPropertyChanging("LastAuth", value)) { _LastAuth = value; OnPropertyChanged("LastAuth"); } } }
private Boolean _IsDeleted;
/// <summary>删除。是否已删除,可恢复</summary>
[DisplayName("删除")]
[Description("删除。是否已删除,可恢复")]
[DataObjectField(false, false, false, 0)]
[BindColumn("IsDeleted", "删除。是否已删除,可恢复", "")]
public Boolean IsDeleted { get => _IsDeleted; set { if (OnPropertyChanging("IsDeleted", value)) { _IsDeleted = value; OnPropertyChanged("IsDeleted"); } } }
private Int32 _CreateUserID;
/// <summary>创建者</summary>
[Category("扩展")]
@ -258,6 +266,7 @@ public partial class App
"Expired" => _Expired,
"Auths" => _Auths,
"LastAuth" => _LastAuth,
"IsDeleted" => _IsDeleted,
"CreateUserID" => _CreateUserID,
"CreateTime" => _CreateTime,
"CreateIP" => _CreateIP,
@ -289,6 +298,7 @@ public partial class App
case "Expired": _Expired = value.ToDateTime(); break;
case "Auths": _Auths = value.ToInt(); break;
case "LastAuth": _LastAuth = value.ToDateTime(); break;
case "IsDeleted": _IsDeleted = value.ToBoolean(); break;
case "CreateUserID": _CreateUserID = value.ToInt(); break;
case "CreateTime": _CreateTime = value.ToDateTime(); break;
case "CreateIP": _CreateIP = Convert.ToString(value); break;
@ -366,6 +376,9 @@ public partial class App
/// <summary>最后请求</summary>
public static readonly Field LastAuth = FindByName("LastAuth");
/// <summary>删除。是否已删除,可恢复</summary>
public static readonly Field IsDeleted = FindByName("IsDeleted");
/// <summary>创建者</summary>
public static readonly Field CreateUserID = FindByName("CreateUserID");
@ -447,6 +460,9 @@ public partial class App
/// <summary>最后请求</summary>
public const String LastAuth = "LastAuth";
/// <summary>删除。是否已删除,可恢复</summary>
public const String IsDeleted = "IsDeleted";
/// <summary>创建者</summary>
public const String CreateUserID = "CreateUserID";

View File

@ -9,6 +9,28 @@ namespace NewLife.Cube.Services;
/// <summary>应用服务</summary>
public class TokenService
{
/// <summary>根据名称查找</summary>
/// <param name="name"></param>
/// <returns></returns>
public App FindByName(String name)
{
var app = App.FindByName(name);
if (app == null || app.IsDeleted) return null;
return app;
}
/// <summary>根据密钥查找</summary>
/// <param name="appKey"></param>
/// <returns></returns>
public App FindBySecret(String appKey)
{
var app = App.FindBySecret(appKey);
if (app == null || app.IsDeleted) return null;
return app;
}
/// <summary>验证应用密码,不存在时新增</summary>
/// <param name="username"></param>
/// <param name="password"></param>
@ -21,7 +43,7 @@ public class TokenService
//if (password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(password));
// 查找应用
var app = App.FindByName(username);
var app = FindByName(username);
// 查找或创建应用,避免多线程创建冲突
app ??= App.GetOrAdd(username, App.FindByName, k => new App
{
@ -45,6 +67,7 @@ public class TokenService
/// <param name="name"></param>
/// <param name="secret"></param>
/// <param name="expire"></param>
/// <param name="id"></param>
/// <returns></returns>
public TokenModel IssueToken(String name, String secret, Int32 expire, String id = null)
{
@ -134,7 +157,7 @@ public class TokenService
if (!jwt.TryDecode(token, out var message)) throw new ApiException(403, $"非法访问[{jwt.Subject}]{message}");
// 验证应用
var app = App.FindByName(jwt.Subject)
var app = FindByName(jwt.Subject)
?? throw new ApiException(403, $"无效应用[{jwt.Subject}]");
if (!app.Enable) throw new ApiException(403, $"已停用应用[{jwt.Subject}]");
@ -161,7 +184,7 @@ public class TokenService
if (!jwt.TryDecode(token, out var message)) ex = new ApiException(403, $"非法访问 {message}");
// 验证应用
var app = App.FindByName(jwt.Subject);
var app = FindByName(jwt.Subject);
if ((app == null || !app.Enable) && ex == null) ex = new ApiException(401, $"无效应用[{jwt.Subject}]");
return (app, ex);

View File

@ -47,7 +47,7 @@ public class OAuthServer
app.Insert();
}
if (!app.Enable) throw new XException("应用[{0}]不可用", client_id);
if (!app.Enable || app.IsDeleted) throw new XException("应用[{0}]不可用", client_id);
if (app.Expired.Year > 2000 && app.Expired < DateTime.Now) throw new XException("应用[{0}]已过期", client_id);
if (!ip.IsNullOrEmpty() && !app.ValidSource(ip)) throw new XException("来源地址不合法 {0}", ip);

View File

@ -12,6 +12,7 @@ using NewLife.Remoting;
using NewLife.Serialization;
using NewLife.Web;
using XCode;
using XCode.Configuration;
using XCode.Membership;
namespace NewLife.Cube;
@ -44,19 +45,34 @@ public class EntityController<TEntity, TModel> : ReadOnlyEntityController<TEntit
{
var url = Request.GetReferer();
var act = "删除";
var entity = FindData(id);
var rs = false;
var err = "";
try
{
if (Valid(entity, DataObjectMethodType.Delete, true))
// 假删除与还原
var fi = GetDeleteField();
if (fi != null)
{
OnDelete(entity);
var restore = GetRequest("restore").ToBoolean();
entity.SetItem(fi.Name, !restore);
if (restore) act = "恢复";
rs = true;
if (Valid(entity, DataObjectMethodType.Update, true))
OnUpdate(entity);
else
err = "验证失败";
}
else
err = "验证失败";
{
if (Valid(entity, DataObjectMethodType.Delete, true))
OnDelete(entity);
else
err = "验证失败";
}
rs = true;
}
catch (Exception ex)
{
@ -65,19 +81,21 @@ public class EntityController<TEntity, TModel> : ReadOnlyEntityController<TEntit
//if (LogOnChange) LogProvider.Provider.WriteLog("Delete", entity, err);
if (Request.IsAjaxRequest())
return JsonRefresh("删除失败!" + err);
return JsonRefresh($"{act}失败!{err}");
throw;
}
if (Request.IsAjaxRequest())
return JsonRefresh(rs ? "删除成功!" : "删除失败!" + err);
return JsonRefresh(rs ? $"{act}成功!" : $"{act}失败!{err}");
else if (!url.IsNullOrEmpty())
return Redirect(url);
else
return RedirectToAction("Index");
}
private static FieldItem GetDeleteField() => Factory.Fields.FirstOrDefault(e => e.Name.EqualIgnoreCase("Deleted", "IsDelete", "IsDeleted") && e.Type == typeof(Boolean));
/// <summary>表单,添加/修改</summary>
/// <returns></returns>
[EntityAuthorize(PermissionFlags.Insert)]
@ -639,10 +657,14 @@ public class EntityController<TEntity, TModel> : ReadOnlyEntityController<TEntit
[DisplayName("删除选中")]
public virtual ActionResult DeleteSelect()
{
var count = 0;
var total = 0;
var success = 0;
var keys = SelectKeys;
if (keys != null && keys.Length > 0)
{
// 假删除
var fi = GetDeleteField();
using var tran = Entity<TEntity>.Meta.CreateTrans();
var list = new List<IEntity>();
foreach (var item in keys)
@ -651,15 +673,28 @@ public class EntityController<TEntity, TModel> : ReadOnlyEntityController<TEntit
if (entity != null)
{
// 验证数据权限
if (Valid(entity, DataObjectMethodType.Delete, true)) list.Add(entity);
count++;
if (fi != null)
{
entity.SetItem(fi.Name, true);
if (Valid(entity, DataObjectMethodType.Update, true)) list.Add(entity);
}
else
{
if (Valid(entity, DataObjectMethodType.Delete, true)) list.Add(entity);
}
}
}
list.Delete();
total = list.Count;
if (fi != null)
success = list.Update();
else
success = list.Delete();
tran.Commit();
}
return JsonRefresh($"共删除{count}行数据");
return JsonRefresh($"共删除{total}行数据,成功{success}行");
}
/// <summary>删除全部</summary>
@ -670,7 +705,11 @@ public class EntityController<TEntity, TModel> : ReadOnlyEntityController<TEntit
{
var url = Request.GetReferer();
var count = 0;
// 假删除
var fi = GetDeleteField();
var total = 0;
var success = 0;
var p = Session[CacheKey] as Pager;
p = new Pager(p);
if (p != null)
@ -683,25 +722,37 @@ public class EntityController<TEntity, TModel> : ReadOnlyEntityController<TEntit
// 不要查记录数
p.RetrieveTotalCount = false;
var list = SearchData(p).ToList();
if (list.Count == 0) break;
var data = SearchData(p).ToList();
if (data.Count == 0) break;
total += data.Count;
count += list.Count;
//list.Delete();
using var tran = Entity<TEntity>.Meta.CreateTrans();
var list2 = new List<IEntity>();
foreach (var entity in list)
var list = new List<IEntity>();
foreach (var entity in data)
{
// 验证数据权限
if (Valid(entity, DataObjectMethodType.Delete, true)) list2.Add(entity);
if (fi != null)
{
entity.SetItem(fi.Name, true);
if (Valid(entity, DataObjectMethodType.Update, true)) list.Add(entity);
}
else
{
if (Valid(entity, DataObjectMethodType.Delete, true)) list.Add(entity);
}
}
list2.Delete();
if (fi != null)
success += list.Update();
else
success += list.Delete();
tran.Commit();
}
}
if (Request.IsAjaxRequest())
return JsonRefresh($"共删除{count}行数据");
return JsonRefresh($"共删除{total}行数据,成功{success}行");
else if (!url.IsNullOrEmpty())
return Redirect(url);
else

View File

@ -605,7 +605,7 @@ public class ReadOnlyEntityController<TEntity> : ControllerBaseX where TEntity :
var app = App.FindBySecret(token);
if (app != null)
{
if (!app.Enable) throw new XException("非法授权!");
if (!app.Enable || app.IsDeleted) throw new XException("非法授权!");
return app?.ToString();
}

View File

@ -41,7 +41,7 @@ namespace NewLife.Cube.ViewModels
/// 提供者
/// </summary>
public List<OAuthConfigModel> Providers =>
OAuthConfig.FindAllWithCache().Where(w=>w.Enable).Select(s =>
OAuthConfig.GetVisibles().Select(s =>
{
var m = new OAuthConfigModel();
m.Copy(s);

View File

@ -31,6 +31,7 @@ else if (this.Has(PermissionFlags.Detail))
var fi = (fact == null || fact.Fields == null) ? null : fact.Fields.FirstOrDefault(e => e.Name.EqualIgnoreCase("Deleted", "IsDelete", "IsDeleted"));
if (fi != null && fi.Type == typeof(Boolean) && (Boolean)entity[fi.Name])
{
rv["restore"] = 1;
<i class="glyphicon glyphicon-transfer" style="color: green;"></i>
<a href="@Url.Action("Delete", rv)" data-action="action" data-confirm="确认恢复?">恢复</a>
}