✨ Magic spell for one time code
🗑️ Drop the usage of casbin ♻️ Refactor the permission service ♻️ Refactor the flow of creating an account 🧱 Email infra structure
This commit is contained in:
		| @@ -1,5 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| @@ -18,18 +19,24 @@ namespace DysonNetwork.Sphere.Permission; | ||||
| /// and when the permission node has a GroupId, the actor will be set to the group, but it won't work on checking | ||||
| /// expect the member of that permission group inherent the permission from the group. | ||||
| [Index(nameof(Key), nameof(Area), nameof(Actor))] | ||||
| public class PermissionNode : ModelBase | ||||
| public class PermissionNode : ModelBase, IDisposable | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Actor { get; set; } = null!; | ||||
|     [MaxLength(1024)] public string Area { get; set; } = null!; | ||||
|     [MaxLength(1024)] public string Key { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public object Value { get; set; } = null!; | ||||
|     [Column(TypeName = "jsonb")] public JsonDocument Value { get; set; } = null!; | ||||
|     public Instant? ExpiredAt { get; set; } = null; | ||||
|     public Instant? AffectedAt { get; set; } = null; | ||||
|      | ||||
|  | ||||
|     public Guid? GroupId { get; set; } = null; | ||||
|     [JsonIgnore] public PermissionNode? Group { get; set; } = null; | ||||
|     [JsonIgnore] public PermissionGroup? Group { get; set; } = null; | ||||
|  | ||||
|     public void Dispose() | ||||
|     { | ||||
|         Value.Dispose(); | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public class PermissionGroup : ModelBase | ||||
| @@ -44,10 +51,9 @@ public class PermissionGroup : ModelBase | ||||
| public class PermissionGroupMember : ModelBase | ||||
| { | ||||
|     public Guid GroupId { get; set; } | ||||
|     public long AccountId { get; set; } | ||||
|     public PermissionGroup Group { get; set; } = null!; | ||||
|     public Account.Account Account { get; set; } = null!; | ||||
|      | ||||
|     public Instant? ExpiredAt { get; set; } = null; | ||||
|     public Instant? AffectedAt { get; set; } = null; | ||||
|     [MaxLength(1024)] public string Actor { get; set; } = null!; | ||||
|  | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|     public Instant? AffectedAt { get; set; } | ||||
| } | ||||
							
								
								
									
										51
									
								
								DysonNetwork.Sphere/Permission/PermissionMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								DysonNetwork.Sphere/Permission/PermissionMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| namespace DysonNetwork.Sphere.Permission; | ||||
|  | ||||
| using System; | ||||
|  | ||||
| [AttributeUsage(AttributeTargets.Method, Inherited = true)] | ||||
| public class RequiredPermissionAttribute(string area, string key) : Attribute | ||||
| { | ||||
|     public string Area { get; set; } = area; | ||||
|     public string Key { get; } = key; | ||||
| } | ||||
|  | ||||
| public class PermissionMiddleware(RequestDelegate next) | ||||
| { | ||||
|     public async Task InvokeAsync(HttpContext httpContext, PermissionService pm) | ||||
|     { | ||||
|         var endpoint = httpContext.GetEndpoint(); | ||||
|  | ||||
|         var attr = endpoint?.Metadata | ||||
|             .OfType<RequiredPermissionAttribute>() | ||||
|             .FirstOrDefault(); | ||||
|  | ||||
|         if (attr != null) | ||||
|         { | ||||
|             if (httpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|             { | ||||
|                 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; | ||||
|                 await httpContext.Response.WriteAsync("Unauthorized"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (currentUser.IsSuperuser) | ||||
|             { | ||||
|                 // Bypass the permission check for performance | ||||
|                 await next(httpContext); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var actor = $"user:{currentUser.Id}"; | ||||
|             var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Area, attr.Key); | ||||
|  | ||||
|             if (!permNode) | ||||
|             { | ||||
|                 httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; | ||||
|                 await httpContext.Response.WriteAsync($"Permission {attr.Area}/{attr.Key} = {true} was required."); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         await next(httpContext); | ||||
|     }  | ||||
| } | ||||
							
								
								
									
										126
									
								
								DysonNetwork.Sphere/Permission/PermissionService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								DysonNetwork.Sphere/Permission/PermissionService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| using System.Text.Json; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Permission; | ||||
|  | ||||
| public class PermissionService(AppDatabase db) | ||||
| { | ||||
|     public async Task<T?> GetPermissionAsync<T>(string actor, string area, string key) | ||||
|     { | ||||
|         var now =  SystemClock.Instance.GetCurrentInstant(); | ||||
|         var groupsId = await db.PermissionGroupMembers | ||||
|             .Where(n => n.Actor == actor) | ||||
|             .Where(n => n.ExpiredAt == null || n.ExpiredAt < now) | ||||
|             .Where(n => n.AffectedAt == null || n.AffectedAt >= now) | ||||
|             .Select(e => e.GroupId) | ||||
|             .ToListAsync(); | ||||
|         var permission = await db.PermissionNodes | ||||
|             .Where(n => n.GroupId == null || groupsId.Contains(n.GroupId.Value)) | ||||
|             .Where(n => n.Key == key && n.Actor == actor && n.Area == area) | ||||
|             .Where(n => n.ExpiredAt == null || n.ExpiredAt < now) | ||||
|             .Where(n => n.AffectedAt == null || n.AffectedAt >= now) | ||||
|             .FirstOrDefaultAsync(); | ||||
|          | ||||
|         return permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default; | ||||
|     } | ||||
|      | ||||
|     public async Task<PermissionNode> AddPermissionNode<T>( | ||||
|         string actor, | ||||
|         string area, | ||||
|         string key, | ||||
|         T value, | ||||
|         Instant? expiredAt = null, | ||||
|         Instant? affectedAt = null | ||||
|     ) | ||||
|     { | ||||
|         if (value is null) throw new ArgumentNullException(nameof(value)); | ||||
|  | ||||
|         var node = new PermissionNode | ||||
|         { | ||||
|             Actor = actor, | ||||
|             Key = key, | ||||
|             Area = area, | ||||
|             Value = _SerializePermissionValue(value), | ||||
|             ExpiredAt = expiredAt, | ||||
|             AffectedAt = affectedAt | ||||
|         }; | ||||
|  | ||||
|         db.PermissionNodes.Add(node); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return node; | ||||
|     } | ||||
|  | ||||
|     public async Task<PermissionNode> AddPermissionNodeToGroup<T>( | ||||
|         PermissionGroup group, | ||||
|         string actor, | ||||
|         string area, | ||||
|         string key, | ||||
|         T value, | ||||
|         Instant? expiredAt = null, | ||||
|         Instant? affectedAt = null | ||||
|     ) | ||||
|     { | ||||
|         if (value is null) throw new ArgumentNullException(nameof(value)); | ||||
|  | ||||
|         var node = new PermissionNode | ||||
|         { | ||||
|             Actor = actor, | ||||
|             Key = key, | ||||
|             Area = area, | ||||
|             Value = _SerializePermissionValue(value), | ||||
|             ExpiredAt = expiredAt, | ||||
|             AffectedAt = affectedAt, | ||||
|             Group = group, | ||||
|             GroupId = group.Id | ||||
|         }; | ||||
|  | ||||
|         db.PermissionNodes.Add(node); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return node; | ||||
|     } | ||||
|  | ||||
|     public async Task RemovePermissionNode(string actor, string area, string key) | ||||
|     { | ||||
|         var node = await db.PermissionNodes | ||||
|             .Where(n => n.Actor == actor && n.Area == area && n.Key == key) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (node is not null) db.PermissionNodes.Remove(node); | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     public async Task RemovePermissionNodeFromGroup<T>(PermissionGroup group, string actor, string area, string key) | ||||
|     { | ||||
|         var node = await db.PermissionNodes | ||||
|             .Where(n => n.GroupId == group.Id) | ||||
|             .Where(n => n.Actor == actor &&  n.Area == area && n.Key == key) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (node is null) return; | ||||
|         db.PermissionNodes.Remove(node); | ||||
|         await db.SaveChangesAsync(); | ||||
|     } | ||||
|  | ||||
|     private static T? _DeserializePermissionValue<T>(JsonDocument json) | ||||
|     { | ||||
|         return JsonSerializer.Deserialize<T>(json.RootElement.GetRawText()); | ||||
|     } | ||||
|  | ||||
|     private static JsonDocument _SerializePermissionValue<T>(T obj) | ||||
|     { | ||||
|         var str = JsonSerializer.Serialize(obj); | ||||
|         return JsonDocument.Parse(str); | ||||
|     } | ||||
|  | ||||
|     public static PermissionNode NewPermissionNode<T>(string actor, string area, string key, T value) | ||||
|     { | ||||
|         return new PermissionNode | ||||
|         { | ||||
|             Actor = actor, | ||||
|             Area = area, | ||||
|             Key = key, | ||||
|             Value = _SerializePermissionValue(value), | ||||
|         }; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user