Compare commits

...

112 Commits

Author SHA1 Message Date
4242953969 ♻️ Re-create the migrations for the Pass 2025-12-14 17:31:21 +08:00
c9530ac8b5 🚚 Rename GeoIP service 2025-12-14 03:19:08 +08:00
4ba7d38d78 📝 Update README 2025-12-14 03:14:40 +08:00
8642737a07 Configurable post page 2025-12-12 00:10:57 +08:00
8181938aaf Managed mode page will render with layout 2025-12-11 22:25:40 +08:00
922afc2239 🐛 Fix realm query 2025-12-10 22:59:18 +08:00
a071bd2738 Publication site global config data structure 2025-12-10 19:33:00 +08:00
43945fc524 🐛 Fix discovery realms order incorrect 2025-12-07 14:28:41 +08:00
e477429a35 👔 Increase the chance of other type of activities show up
🗑️ Remove debug include in timeline
2025-12-06 21:12:08 +08:00
fe3a057185 👔 Discovery realms will show desc by member count 2025-12-06 21:10:08 +08:00
ad3c104c5c Proper trace for auth session 2025-12-04 00:38:44 +08:00
2020d625aa 🗃️ Add migration of add sticker pack icon 2025-12-04 00:27:09 +08:00
f471c5635d Post article thumbnail 2025-12-04 00:26:54 +08:00
eaeaa28c60 Sticker icon 2025-12-04 00:19:36 +08:00
ee5c7cb7ce 🐛 Fix get device API 2025-12-03 23:29:31 +08:00
33abf12e41 🐛 Fix pass service swagger docs duplicate schema name cause 500 2025-12-03 22:46:47 +08:00
4a71f92ef0 ♻️ Updated auth challenges and device API to fit new design 2025-12-03 22:43:35 +08:00
4faa1a4b64 🐛 Fix message pack cache serilaize issue in sticker 2025-12-03 22:09:56 +08:00
e49a1ec49a Push token clean up when invalid 2025-12-03 21:42:18 +08:00
a88f42b26a Rolling back to old logic to provide mock device id in websocket gateway 2025-12-03 21:30:29 +08:00
c45be62331 Support switching from JSON to MessagePack in cache during runtime 2025-12-03 21:27:26 +08:00
c8228e0c8e Use JSON to serialize cache 2025-12-03 01:47:57 +08:00
c642c6d646 Resend self activation email API 2025-12-03 01:17:39 +08:00
270c211cb8 ♻️ Refactored to make a simplifier auth session system 2025-12-03 00:38:28 +08:00
74c8f3490d 🐛 Fix the message pack serializer 2025-12-03 00:38:12 +08:00
b364edc74b Use Json Serializer in cache again 2025-12-02 22:59:43 +08:00
9addf38677 🐛 Enable contractless serilization in cache to fix message pack serilizer 2025-12-02 22:51:12 +08:00
a02ed10434 🐛 Fix use wrong DI type in cache service 2025-12-02 22:45:30 +08:00
aca28f9318 ♻️ Refactored the cache service 2025-12-02 22:38:47 +08:00
c2f72993b7 🐛 Fix app snapshot didn't included in release 2025-12-02 21:52:24 +08:00
158cc75c5b 💥 Simplified permission node system and data structure 2025-12-02 21:42:26 +08:00
fa2f53ff7a 🐛 Fix file reference created with wrong date 2025-12-02 21:03:57 +08:00
2cce5ebf80 Use affiliation spell for registeration 2025-12-02 00:54:57 +08:00
13b2e46ecc Affliation spell CRUD 2025-12-01 23:33:48 +08:00
cbd68c9ae6 Proper site manager send file method 2025-12-01 22:55:20 +08:00
b99b61e0f9 🐛 Fix chat backward comapbility 2025-11-30 21:33:39 +08:00
94f4e68120 Timeout prevent send message logic 2025-11-30 21:13:54 +08:00
d5510f7e4d Chat timeout APIs
🐛 Fix member listing in chat
2025-11-30 21:08:07 +08:00
c038ab9e3c ♻️ A more robust and simpler chat system 2025-11-30 20:58:48 +08:00
e97719ec84 🗃️ Add missing account id migrations 2025-11-30 20:13:15 +08:00
40b8ea8eb8 🗃️ Bring account id back to chat room 2025-11-30 19:59:30 +08:00
f9b4dd45d7 🐛 Trying to fix relationship bugs 2025-11-30 17:52:19 +08:00
a46de4662c 🐛 Fix gateway 2025-11-30 17:51:27 +08:00
fdd14b860e 🐛 Fix wrong required status of validate account create request 2025-11-30 17:37:34 +08:00
cb62df81e2 👔 Adjust lookup account logic 2025-11-30 17:20:20 +08:00
46717e39a7 Admin delete account endpoint 2025-11-30 17:19:33 +08:00
344ed6e348 Account validation endpoint 2025-11-30 17:16:11 +08:00
a8b62fb0eb Auth via authorized device 2025-11-30 00:00:13 +08:00
00b3087d6a ♻️ Refactored auth service for better security 2025-11-29 18:00:23 +08:00
78f3873a0c 🐛 Fix birthday check in 2025-11-27 22:22:22 +08:00
a7f4173df7 Special birthday check in tips 2025-11-27 21:49:25 +08:00
f51c3c1724 🐛 Fix birthday check in result didn't show up 2025-11-27 21:41:30 +08:00
a92dc7e140 👔 Remove single file 1MB limit in site 2025-11-24 22:54:16 +08:00
c42befed6b ♻️ Refactored notification meta 2025-11-23 13:20:40 +08:00
2b95d58611 All unread messages endpoint 2025-11-23 12:28:57 +08:00
726a752fbb :zsap: Pagination in chat sync 2025-11-23 12:07:58 +08:00
2024972832 🐛 Trying to fix Pass service issues 2025-11-23 03:02:51 +08:00
d553ca2ca7 🐛 Dozens of bug fixes in chat 2025-11-23 01:17:15 +08:00
aeef16495f 🐛 Fix sitemap and rss still respond all types of posts 2025-11-22 18:55:29 +08:00
9b26a2a7eb 🐛 Fix replace of markdown convertion 2025-11-22 18:53:48 +08:00
2317033dae 👔 Stop rendering post attachments in article post on hosted pages 2025-11-22 18:24:32 +08:00
fd6e9c9780 🐛 Fix some stupid bugs 2025-11-22 18:22:53 +08:00
af0a2ff493 💄 Enrich post susbcription notification 2025-11-22 18:08:11 +08:00
b142a71c32 🐛 Fix publisher member didn't include publisher in response 2025-11-22 17:57:17 +08:00
27e3cc853a 🐛 Fix post service grpc call made type filter wrong 2025-11-22 17:55:45 +08:00
590519c28f 🐛 Fix index shows all type of posts in managed page 2025-11-22 17:53:52 +08:00
8ccf8100d4 👔 Make listing on the hosted page shows article only 2025-11-22 17:50:19 +08:00
ec21a94921 🐛 Serval bug fixes in hosted page 2025-11-22 17:43:52 +08:00
7b7a6c9218 Extend the ability of the hosted page markdown parser 2025-11-22 17:40:17 +08:00
0e44d9c514 🐛 Fix publisher invite controller still use int user id 2025-11-22 17:25:45 +08:00
e449e16d33 🐛 Fix pagination overflow in hosted page 2025-11-22 17:20:36 +08:00
3ce2b36c15 🐛 Fix featured post on hosted page uses wrong order 2025-11-22 17:13:02 +08:00
f7388822e0 🐛 Unable to use random split in open fund 2025-11-22 16:54:29 +08:00
3800dae8b7 SEO optimization on the hosted pages 2025-11-22 16:45:44 +08:00
c62ed191f3 File deploy smart mode 2025-11-22 16:00:30 +08:00
8b77f0e0ad Site management purge files and deploy from zip 2025-11-22 15:50:20 +08:00
2b56c6f1e5 Static site hosting support access directory as index.html 2025-11-22 15:49:29 +08:00
ef02265ccd 💄 Optimize hosted page index 2025-11-22 14:16:40 +08:00
f4505d2ecc 💄 Add titles to the hosted pages 2025-11-22 13:49:26 +08:00
9d2242d331 💄 Hosted page SEO optimization 2025-11-22 13:42:13 +08:00
c806365a81 Render markdown on hosted pages 2025-11-22 13:28:37 +08:00
bd1715c9a3 💄 Optimize hosted post details page 2025-11-22 13:17:34 +08:00
0b0598712e 💄 Updated the hosted site post page 2025-11-22 13:09:57 +08:00
92a4899e7c The posts page basis 2025-11-22 02:33:22 +08:00
bdc8db3091 About page also contains site info 2025-11-22 02:18:57 +08:00
a16da37221 Account about page 2025-11-22 01:47:10 +08:00
70a18b07ff 🐛 Bug fixes in the publication site hosting 2025-11-21 23:36:38 +08:00
98b8d5f33b ♻️ New error page 2025-11-21 23:30:43 +08:00
2a35786204 🐛 Fix self-managed files hosting 2025-11-21 22:27:27 +08:00
7016a0a943 Render self-managed site 2025-11-21 01:55:22 +08:00
cad72502d9 Managed page rendering 2025-11-21 01:41:25 +08:00
226a64df41 💄 Optimize the page rendering in zone 2025-11-21 01:21:40 +08:00
75b8567a28 🐛 Fix file management of the site 2025-11-21 00:40:58 +08:00
3aa5561a07 🐛 Fix hosted sites 2025-11-20 23:47:41 +08:00
c0ebb496fe Site manager 2025-11-20 22:54:24 +08:00
afccb27bd4 Site mode 2025-11-20 22:40:36 +08:00
6ed96780ab 💥 Improvements in the URL of the publication site 2025-11-20 21:29:32 +08:00
8e5cdfbc62 Zone site placeholder 2025-11-19 23:14:22 +08:00
1b774c1de6 ♻️ Moved the site to the Zone project 2025-11-19 22:34:01 +08:00
9b4cbade5c :heavy_plus_arrow: Add alpine.js to zone 2025-11-19 22:05:26 +08:00
a52e54f672 🔨 Setup the docker build for tailwindcss 2025-11-19 22:02:26 +08:00
aa48d5e25d 🔨 Setup the tailwindcss and daisyui frontend for the zone 2025-11-19 21:33:14 +08:00
ce18b194a5 🔨 Finish the initial setup of the Zone project 2025-11-19 21:12:07 +08:00
382579a20e 🎉 Initial commit for the Zone project 2025-11-19 21:04:44 +08:00
18d50346a9 👔 Update publication site limits for perk members 2025-11-19 00:48:36 +08:00
ac51bbde6c Publication Sites aka Solian Pages 2025-11-18 23:39:00 +08:00
4ab0dcf1c2 🐛 Fix file reference JSON loop 2025-11-18 21:52:21 +08:00
587066d847 Delete files in batch API 2025-11-18 20:33:02 +08:00
faa375042a New drive api order etc 2025-11-18 18:50:39 +08:00
65b6f3a606 🐛 Fix bugs 2025-11-18 18:40:23 +08:00
fa1a40c637 File references listing endpoint 2025-11-18 01:06:02 +08:00
d43ce7cb11 🗑️ Remove the fast upload endpoint 2025-11-18 00:55:29 +08:00
246 changed files with 21007 additions and 35514 deletions

View File

@@ -27,8 +27,8 @@ jobs:
run: | run: |
files="${{ steps.changed-files.outputs.files }}" files="${{ steps.changed-files.outputs.files }}"
matrix="{\"include\":[]}" matrix="{\"include\":[]}"
services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight") services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone")
images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight") images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone")
changed_services=() changed_services=()
for file in $files; do for file in $files; do

View File

@@ -26,11 +26,17 @@ var insightService = builder.AddProject<Projects.DysonNetwork_Insight>("insight"
.WithReference(ringService) .WithReference(ringService)
.WithReference(sphereService) .WithReference(sphereService)
.WithReference(developService); .WithReference(developService);
var zoneService = builder.AddProject<Projects.DysonNetwork_Zone>("zone")
.WithReference(passService)
.WithReference(ringService)
.WithReference(sphereService)
.WithReference(developService)
.WithReference(insightService);
passService.WithReference(developService).WithReference(driveService); passService.WithReference(developService).WithReference(driveService);
List<IResourceBuilder<ProjectResource>> services = List<IResourceBuilder<ProjectResource>> services =
[ringService, passService, driveService, sphereService, developService, insightService]; [ringService, passService, driveService, sphereService, developService, insightService, zoneService];
for (var idx = 0; idx < services.Count; idx++) for (var idx = 0; idx < services.Count; idx++)
{ {

View File

@@ -1,28 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="13.0.0" /> <Sdk Name="Aspire.AppHost.Sdk" Version="13.0.0"/>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId> <UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
<RootNamespace>DysonNetwork.Control</RootNamespace> <RootNamespace>DysonNetwork.Control</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.0.0" /> <PackageReference Include="Aspire.Hosting.AppHost" Version="13.0.0"/>
<PackageReference Include="Aspire.Hosting.Docker" Version="13.0.0-preview.1.25560.3" /> <PackageReference Include="Aspire.Hosting.Docker" Version="13.0.0-preview.1.25560.3"/>
<PackageReference Include="Aspire.Hosting.Nats" Version="13.0.0" /> <PackageReference Include="Aspire.Hosting.Nats" Version="13.0.0"/>
<PackageReference Include="Aspire.Hosting.Redis" Version="13.0.0" /> <PackageReference Include="Aspire.Hosting.Redis" Version="13.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" /> <ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj"/>
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" /> <ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj"/>
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" /> <ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj"/>
<ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" /> <ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj"/>
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" /> <ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj"/>
<ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj" /> <ProjectReference Include="..\DysonNetwork.Gateway\DysonNetwork.Gateway.csproj"/>
<ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj" /> <ProjectReference Include="..\DysonNetwork.Insight\DysonNetwork.Insight.csproj"/>
</ItemGroup> <ProjectReference Include="..\DysonNetwork.Zone\DysonNetwork.Zone.csproj"/>
</ItemGroup>
</Project> </Project>

View File

@@ -69,7 +69,7 @@ public class DeveloperController(
[HttpPost("{name}/enroll")] [HttpPost("{name}/enroll")]
[Authorize] [Authorize]
[RequiredPermission("global", "developers.create")] [AskPermission("developers.create")]
public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name) public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();

View File

@@ -16,7 +16,7 @@ public static class ApplicationConfiguration
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseMiddleware<PermissionMiddleware>(); app.UseMiddleware<RemotePermissionMiddleware>();
app.MapControllers(); app.MapControllers();

View File

@@ -16,9 +16,7 @@ public static class ServiceCollectionExtensions
services.AddLocalization(); services.AddLocalization();
services.AddDbContext<AppDatabase>(); services.AddDbContext<AppDatabase>();
services.AddSingleton<IClock>(SystemClock.Instance);
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddSingleton<ICacheService, CacheServiceRedis>();
services.AddHttpClient(); services.AddHttpClient();

View File

@@ -1,22 +1,28 @@
{ {
"Debug": true, "Debug": true,
"BaseUrl": "http://localhost:5071", "BaseUrl": "http://localhost:5071",
"SiteUrl": "https://solian.app", "SiteUrl": "https://solian.app",
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
},
"KnownProxies": [
"127.0.0.1",
"::1"
],
"Swagger": {
"PublicBasePath": "/develop"
},
"Cache": {
"Serializer": "MessagePack"
},
"Etcd": {
"Insecure": true
} }
},
"AllowedHosts": "*",
"ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
},
"KnownProxies": ["127.0.0.1", "::1"],
"Swagger": {
"PublicBasePath": "/develop"
},
"Etcd": {
"Insecure": true
}
} }

View File

@@ -6,7 +6,6 @@ using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Query;
using NodaTime; using NodaTime;
using Quartz; using Quartz;
using TaskStatus = DysonNetwork.Drive.Storage.Model.TaskStatus; using TaskStatus = DysonNetwork.Drive.Storage.Model.TaskStatus;
@@ -24,7 +23,7 @@ public class AppDatabase(
public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!; public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!;
public DbSet<SnCloudFile> Files { get; set; } = null!; public DbSet<SnCloudFile> Files { get; set; } = null!;
public DbSet<CloudFileReference> FileReferences { get; set; } = null!; public DbSet<SnCloudFileReference> FileReferences { get; set; } = null!;
public DbSet<SnCloudFileIndex> FileIndexes { get; set; } public DbSet<SnCloudFileIndex> FileIndexes { get; set; }
public DbSet<PersistentTask> Tasks { get; set; } = null!; public DbSet<PersistentTask> Tasks { get; set; } = null!;

View File

@@ -24,9 +24,16 @@ public class FileIndexController(
/// </summary> /// </summary>
/// <param name="path">The path to browse (defaults to root "/")</param> /// <param name="path">The path to browse (defaults to root "/")</param>
/// <param name="query">Optional query to filter files by name</param> /// <param name="query">Optional query to filter files by name</param>
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
/// <returns>List of files in the specified path</returns> /// <returns>List of files in the specified path</returns>
[HttpGet("browse")] [HttpGet("browse")]
public async Task<IActionResult> BrowseFiles([FromQuery] string path = "/", [FromQuery] string? query = null) public async Task<IActionResult> BrowseFiles(
[FromQuery] string path = "/",
[FromQuery] string? query = null,
[FromQuery] string order = "date",
[FromQuery] bool orderDesc = true
)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
@@ -44,6 +51,17 @@ public class FileIndexController(
.ToList(); .ToList();
} }
// Apply sorting
fileIndexes = order.ToLower() switch
{
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
};
// Get all file indexes for this account to extract child folders // Get all file indexes for this account to extract child folders
var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId); var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
@@ -109,9 +127,15 @@ public class FileIndexController(
/// Gets all files for the current user (across all paths) /// Gets all files for the current user (across all paths)
/// </summary> /// </summary>
/// <param name="query">Optional query to filter files by name</param> /// <param name="query">Optional query to filter files by name</param>
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
/// <returns>List of all files for the user</returns> /// <returns>List of all files for the user</returns>
[HttpGet("all")] [HttpGet("all")]
public async Task<IActionResult> GetAllFiles([FromQuery] string? query = null) public async Task<IActionResult> GetAllFiles(
[FromQuery] string? query = null,
[FromQuery] string order = "date",
[FromQuery] bool orderDesc = true
)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
@@ -129,6 +153,17 @@ public class FileIndexController(
.ToList(); .ToList();
} }
// Apply sorting
fileIndexes = order.ToLower() switch
{
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
};
return Ok(new return Ok(new
{ {
Files = fileIndexes, Files = fileIndexes,
@@ -154,6 +189,9 @@ public class FileIndexController(
/// <param name="offset">The number of files to skip</param> /// <param name="offset">The number of files to skip</param>
/// <param name="take">The number of files to return</param> /// <param name="take">The number of files to return</param>
/// <param name="pool">The pool ID of those files</param> /// <param name="pool">The pool ID of those files</param>
/// <param name="query">Optional query to filter files by name</param>
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
/// <returns>List of unindexed files</returns> /// <returns>List of unindexed files</returns>
[HttpGet("unindexed")] [HttpGet("unindexed")]
public async Task<IActionResult> GetUnindexedFiles( public async Task<IActionResult> GetUnindexedFiles(
@@ -161,7 +199,9 @@ public class FileIndexController(
[FromQuery] bool recycled = false, [FromQuery] bool recycled = false,
[FromQuery] int offset = 0, [FromQuery] int offset = 0,
[FromQuery] int take = 20, [FromQuery] int take = 20,
[FromQuery] string? query = null [FromQuery] string? query = null,
[FromQuery] string order = "date",
[FromQuery] bool orderDesc = true
) )
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
@@ -176,9 +216,19 @@ public class FileIndexController(
&& f.IsMarkedRecycle == recycled && f.IsMarkedRecycle == recycled
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId) && !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)
) )
.OrderByDescending(f => f.CreatedAt)
.AsQueryable(); .AsQueryable();
// Apply sorting
filesQuery = order.ToLower() switch
{
"name" => orderDesc ? filesQuery.OrderByDescending(f => f.Name)
: filesQuery.OrderBy(f => f.Name),
"size" => orderDesc ? filesQuery.OrderByDescending(f => f.Size)
: filesQuery.OrderBy(f => f.Size),
_ => orderDesc ? filesQuery.OrderByDescending(f => f.CreatedAt)
: filesQuery.OrderBy(f => f.CreatedAt)
};
if (pool.HasValue) filesQuery = filesQuery.Where(f => f.PoolId == pool); if (pool.HasValue) filesQuery = filesQuery.Where(f => f.PoolId == pool);
if (!string.IsNullOrWhiteSpace(query)) if (!string.IsNullOrWhiteSpace(query))

View File

@@ -179,7 +179,7 @@ namespace DysonNetwork.Drive.Migrations
b.UseTphMappingStrategy(); b.UseTphMappingStrategy();
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -571,7 +571,7 @@ namespace DysonNetwork.Drive.Migrations
b.HasDiscriminator().HasValue("PersistentUploadTask"); b.HasDiscriminator().HasValue("PersistentUploadTask");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileReference", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File") b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
.WithMany("References") .WithMany("References")

View File

@@ -12,9 +12,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase
services.AddSingleton<IClock>(SystemClock.Instance);
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis
services.AddHttpClient(); services.AddHttpClient();

View File

@@ -1,4 +1,3 @@
using DysonNetwork.Drive.Billing;
using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
@@ -14,9 +13,9 @@ namespace DysonNetwork.Drive.Storage;
public class FileController( public class FileController(
AppDatabase db, AppDatabase db,
FileService fs, FileService fs,
QuotaService qs,
IConfiguration configuration, IConfiguration configuration,
IWebHostEnvironment env IWebHostEnvironment env,
FileReferenceService fileReferenceService
) : ControllerBase ) : ControllerBase
{ {
[HttpGet("{id}")] [HttpGet("{id}")]
@@ -231,6 +230,21 @@ public class FileController(
return file; return file;
} }
[HttpGet("{id}/references")]
public async Task<ActionResult<List<Shared.Models.SnCloudFileReference>>> GetFileReferences(string id)
{
var file = await fs.GetFileAsync(id);
if (file is null) return NotFound("File not found.");
// Check if user has access to the file
var accessResult = await ValidateFileAccess(file, null);
if (accessResult is not null) return accessResult;
// Get references using the injected FileReferenceService
var references = await fileReferenceService.GetReferencesAsync(id);
return Ok(references);
}
[Authorize] [Authorize]
[HttpPatch("{id}/name")] [HttpPatch("{id}/name")]
public async Task<ActionResult<SnCloudFile>> UpdateFileName(string id, [FromBody] string name) public async Task<ActionResult<SnCloudFile>> UpdateFileName(string id, [FromBody] string name)
@@ -279,7 +293,9 @@ public class FileController(
[FromQuery] bool recycled = false, [FromQuery] bool recycled = false,
[FromQuery] int offset = 0, [FromQuery] int offset = 0,
[FromQuery] int take = 20, [FromQuery] int take = 20,
[FromQuery] string? query = null [FromQuery] string? query = null,
[FromQuery] string order = "date",
[FromQuery] bool orderDesc = true
) )
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -289,7 +305,6 @@ public class FileController(
.Where(e => e.IsMarkedRecycle == recycled) .Where(e => e.IsMarkedRecycle == recycled)
.Where(e => e.AccountId == accountId) .Where(e => e.AccountId == accountId)
.Include(e => e.Pool) .Include(e => e.Pool)
.OrderByDescending(e => e.CreatedAt)
.AsQueryable(); .AsQueryable();
if (pool.HasValue) filesQuery = filesQuery.Where(e => e.PoolId == pool); if (pool.HasValue) filesQuery = filesQuery.Where(e => e.PoolId == pool);
@@ -299,6 +314,14 @@ public class FileController(
filesQuery = filesQuery.Where(e => e.Name.Contains(query)); filesQuery = filesQuery.Where(e => e.Name.Contains(query));
} }
filesQuery = order.ToLower() switch
{
"date" => orderDesc ? filesQuery.OrderByDescending(e => e.CreatedAt) : filesQuery.OrderBy(e => e.CreatedAt),
"size" => orderDesc ? filesQuery.OrderByDescending(e => e.Size) : filesQuery.OrderBy(e => e.Size),
"name" => orderDesc ? filesQuery.OrderByDescending(e => e.Name) : filesQuery.OrderBy(e => e.Name),
_ => filesQuery.OrderByDescending(e => e.CreatedAt)
};
var total = await filesQuery.CountAsync(); var total = await filesQuery.CountAsync();
Response.Headers.Append("X-Total", total.ToString()); Response.Headers.Append("X-Total", total.ToString());
@@ -310,6 +333,22 @@ public class FileController(
return Ok(files); return Ok(files);
} }
public class FileBatchDeletionRequest
{
public List<string> FileIds { get; set; } = [];
}
[Authorize]
[HttpPost("batches/delete")]
public async Task<ActionResult> DeleteFileBatch([FromBody] FileBatchDeletionRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var userId = Guid.Parse(currentUser.Id);
var count = await fs.DeleteAccountFileBatchAsync(userId, request.FileIds);
return Ok(new { Count = count });
}
[Authorize] [Authorize]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<ActionResult<SnCloudFile>> DeleteFile(string id) public async Task<ActionResult<SnCloudFile>> DeleteFile(string id)
@@ -342,116 +381,10 @@ public class FileController(
[Authorize] [Authorize]
[HttpDelete("recycle")] [HttpDelete("recycle")]
[RequiredPermission("maintenance", "files.delete.recycle")] [AskPermission("files.delete.recycle")]
public async Task<ActionResult> DeleteAllRecycledFiles() public async Task<ActionResult> DeleteAllRecycledFiles()
{ {
var count = await fs.DeleteAllRecycledFilesAsync(); var count = await fs.DeleteAllRecycledFilesAsync();
return Ok(new { Count = count }); return Ok(new { Count = count });
} }
}
public class CreateFastFileRequest
{
public string Name { get; set; } = null!;
public long Size { get; set; }
public string Hash { get; set; } = null!;
public string? MimeType { get; set; }
public string? Description { get; set; }
public Dictionary<string, object?>? UserMeta { get; set; }
public Dictionary<string, object?>? FileMeta { get; set; }
public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; }
public Guid PoolId { get; set; }
}
[Authorize]
[HttpPost("fast")]
[RequiredPermission("global", "files.create")]
public async Task<ActionResult<SnCloudFile>> CreateFastFile([FromBody] CreateFastFileRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var pool = await db.Pools.FirstOrDefaultAsync(p => p.Id == request.PoolId);
if (pool is null) return BadRequest();
if (!currentUser.IsSuperuser && pool.AccountId != accountId)
return StatusCode(403, "You don't have permission to create files in this pool.");
if (!pool.PolicyConfig.EnableFastUpload)
return StatusCode(
403,
"This pool does not allow fast upload"
);
if (pool.PolicyConfig.RequirePrivilege > 0)
{
if (currentUser.PerkSubscription is null)
{
return StatusCode(
403,
$"You need to have join the Stellar Program to use this pool"
);
}
var privilege =
PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier);
if (privilege < pool.PolicyConfig.RequirePrivilege)
{
return StatusCode(
403,
$"You need Stellar Program tier {pool.PolicyConfig.RequirePrivilege} to use this pool, you are tier {privilege}"
);
}
}
if (request.Size > pool.PolicyConfig.MaxFileSize)
{
return StatusCode(
403,
$"File size {request.Size} is larger than the pool's maximum file size {pool.PolicyConfig.MaxFileSize}"
);
}
var (ok, billableUnit, quota) = await qs.IsFileAcceptable(
accountId,
pool.BillingConfig.CostMultiplier ?? 1.0,
request.Size
);
if (!ok)
{
return StatusCode(
403,
$"File size {billableUnit} is larger than the user's quota {quota}"
);
}
await using var transaction = await db.Database.BeginTransactionAsync();
try
{
var file = new SnCloudFile
{
Name = request.Name,
Size = request.Size,
Hash = request.Hash,
MimeType = request.MimeType,
Description = request.Description,
AccountId = accountId,
UserMeta = request.UserMeta,
FileMeta = request.FileMeta,
SensitiveMarks = request.SensitiveMarks,
PoolId = request.PoolId
};
db.Files.Add(file);
await db.SaveChangesAsync();
await fs._PurgeCacheAsync(file.Id);
await transaction.CommitAsync();
file.FastUploadLink = await fs.CreateFastUploadLinkAsync(file);
return file;
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
}
}

View File

@@ -21,7 +21,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
/// <param name="expiredAt">Optional expiration time for the file</param> /// <param name="expiredAt">Optional expiration time for the file</param>
/// <param name="duration">Optional duration after which the file expires (alternative to expiredAt)</param> /// <param name="duration">Optional duration after which the file expires (alternative to expiredAt)</param>
/// <returns>The created file reference</returns> /// <returns>The created file reference</returns>
public async Task<CloudFileReference> CreateReferenceAsync( public async Task<SnCloudFileReference> CreateReferenceAsync(
string fileId, string fileId,
string usage, string usage,
string resourceId, string resourceId,
@@ -34,7 +34,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
if (duration.HasValue) if (duration.HasValue)
finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value; finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value;
var reference = new CloudFileReference var reference = new SnCloudFileReference
{ {
FileId = fileId, FileId = fileId,
Usage = usage, Usage = usage,
@@ -50,7 +50,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
return reference; return reference;
} }
public async Task<List<CloudFileReference>> CreateReferencesAsync( public async Task<List<SnCloudFileReference>> CreateReferencesAsync(
List<string> fileId, List<string> fileId,
string usage, string usage,
string resourceId, string resourceId,
@@ -58,12 +58,15 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
Duration? duration = null Duration? duration = null
) )
{ {
var data = fileId.Select(id => new CloudFileReference var now = SystemClock.Instance.GetCurrentInstant();
var data = fileId.Select(id => new SnCloudFileReference
{ {
FileId = id, FileId = id,
Usage = usage, Usage = usage,
ResourceId = resourceId, ResourceId = resourceId,
ExpiredAt = expiredAt ?? SystemClock.Instance.GetCurrentInstant() + duration ExpiredAt = expiredAt ?? now + duration,
CreatedAt = now,
UpdatedAt = now
}).ToList(); }).ToList();
await db.BulkInsertAsync(data); await db.BulkInsertAsync(data);
return data; return data;
@@ -74,11 +77,11 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
/// </summary> /// </summary>
/// <param name="fileId">The ID of the file</param> /// <param name="fileId">The ID of the file</param>
/// <returns>A list of all references to the file</returns> /// <returns>A list of all references to the file</returns>
public async Task<List<CloudFileReference>> GetReferencesAsync(string fileId) public async Task<List<SnCloudFileReference>> GetReferencesAsync(string fileId)
{ {
var cacheKey = $"{CacheKeyPrefix}list:{fileId}"; var cacheKey = $"{CacheKeyPrefix}list:{fileId}";
var cachedReferences = await cache.GetAsync<List<CloudFileReference>>(cacheKey); var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
if (cachedReferences is not null) if (cachedReferences is not null)
return cachedReferences; return cachedReferences;
@@ -91,17 +94,17 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
return references; return references;
} }
public async Task<Dictionary<string, List<CloudFileReference>>> GetReferencesAsync(IEnumerable<string> fileIds) public async Task<Dictionary<string, List<SnCloudFileReference>>> GetReferencesAsync(IEnumerable<string> fileIds)
{ {
var fileIdList = fileIds.ToList(); var fileIdList = fileIds.ToList();
var result = new Dictionary<string, List<CloudFileReference>>(); var result = new Dictionary<string, List<SnCloudFileReference>>();
// Check cache for each file ID // Check cache for each file ID
var uncachedFileIds = new List<string>(); var uncachedFileIds = new List<string>();
foreach (var fileId in fileIdList) foreach (var fileId in fileIdList)
{ {
var cacheKey = $"{CacheKeyPrefix}list:{fileId}"; var cacheKey = $"{CacheKeyPrefix}list:{fileId}";
var cachedReferences = await cache.GetAsync<List<CloudFileReference>>(cacheKey); var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
if (cachedReferences is not null) if (cachedReferences is not null)
{ {
result[fileId] = cachedReferences; result[fileId] = cachedReferences;
@@ -159,11 +162,11 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
/// </summary> /// </summary>
/// <param name="resourceId">The ID of the resource</param> /// <param name="resourceId">The ID of the resource</param>
/// <returns>A list of file references associated with the resource</returns> /// <returns>A list of file references associated with the resource</returns>
public async Task<List<CloudFileReference>> GetResourceReferencesAsync(string resourceId) public async Task<List<SnCloudFileReference>> GetResourceReferencesAsync(string resourceId)
{ {
var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}"; var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";
var cachedReferences = await cache.GetAsync<List<CloudFileReference>>(cacheKey); var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
if (cachedReferences is not null) if (cachedReferences is not null)
return cachedReferences; return cachedReferences;
@@ -181,11 +184,11 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
/// </summary> /// </summary>
/// <param name="usage">The usage context</param> /// <param name="usage">The usage context</param>
/// <returns>A list of file references with the specified usage</returns> /// <returns>A list of file references with the specified usage</returns>
public async Task<List<CloudFileReference>> GetUsageReferencesAsync(string usage) public async Task<List<SnCloudFileReference>> GetUsageReferencesAsync(string usage)
{ {
var cacheKey = $"{CacheKeyPrefix}usage:{usage}"; var cacheKey = $"{CacheKeyPrefix}usage:{usage}";
var cachedReferences = await cache.GetAsync<List<CloudFileReference>>(cacheKey); var cachedReferences = await cache.GetAsync<List<SnCloudFileReference>>(cacheKey);
if (cachedReferences is not null) if (cachedReferences is not null)
return cachedReferences; return cachedReferences;
@@ -307,7 +310,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
/// <param name="expiredAt">Optional expiration time for newly added files</param> /// <param name="expiredAt">Optional expiration time for newly added files</param>
/// <param name="duration">Optional duration after which newly added files expire</param> /// <param name="duration">Optional duration after which newly added files expire</param>
/// <returns>A list of the updated file references</returns> /// <returns>A list of the updated file references</returns>
public async Task<List<CloudFileReference>> UpdateResourceFilesAsync( public async Task<List<SnCloudFileReference>> UpdateResourceFilesAsync(
string resourceId, string resourceId,
IEnumerable<string>? newFileIds, IEnumerable<string>? newFileIds,
string usage, string usage,
@@ -315,7 +318,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
Duration? duration = null) Duration? duration = null)
{ {
if (newFileIds == null) if (newFileIds == null)
return new List<CloudFileReference>(); return new List<SnCloudFileReference>();
var existingReferences = await db.FileReferences var existingReferences = await db.FileReferences
.Where(r => r.ResourceId == resourceId && r.Usage == usage) .Where(r => r.ResourceId == resourceId && r.Usage == usage)
@@ -333,7 +336,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
// Files to add // Files to add
var toAdd = newFileIdsList var toAdd = newFileIdsList
.Where(id => !existingFileIds.Contains(id)) .Where(id => !existingFileIds.Contains(id))
.Select(id => new CloudFileReference .Select(id => new SnCloudFileReference
{ {
FileId = id, FileId = id,
Usage = usage, Usage = usage,
@@ -485,7 +488,7 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
/// <param name="resourceId">The resource ID</param> /// <param name="resourceId">The resource ID</param>
/// <param name="usageType">The usage type</param> /// <param name="usageType">The usage type</param>
/// <returns>List of file references</returns> /// <returns>List of file references</returns>
public async Task<List<CloudFileReference>> GetResourceReferencesAsync(string resourceId, string usageType) public async Task<List<SnCloudFileReference>> GetResourceReferencesAsync(string resourceId, string usageType)
{ {
return await db.FileReferences return await db.FileReferences
.Where(r => r.ResourceId == resourceId && r.Usage == usageType) .Where(r => r.ResourceId == resourceId && r.Usage == usageType)

View File

@@ -718,6 +718,21 @@ public class FileService(
return count; return count;
} }
public async Task<int> DeleteAccountFileBatchAsync(Guid accountId, List<string> fileIds)
{
var files = await db.Files
.Where(f => f.AccountId == accountId && fileIds.Contains(f.Id))
.ToListAsync();
var count = files.Count;
var tasks = files.Select(f => DeleteFileDataAsync(f, true));
await Task.WhenAll(tasks);
var fileIdsList = files.Select(f => f.Id).ToList();
await _PurgeCacheRangeAsync(fileIdsList);
db.RemoveRange(files);
await db.SaveChangesAsync();
return count;
}
public async Task<int> DeletePoolRecycledFilesAsync(Guid poolId) public async Task<int> DeletePoolRecycledFilesAsync(Guid poolId)
{ {
var files = await db.Files var files = await db.Files
@@ -788,4 +803,4 @@ file class UpdatableCloudFile(SnCloudFile file)
.SetProperty(f => f.UserMeta, userMeta) .SetProperty(f => f.UserMeta, userMeta)
.SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle); .SetProperty(f => f.IsMarkedRecycle, IsMarkedRecycle);
} }
} }

View File

@@ -113,7 +113,7 @@ public class FileUploadController(
if (currentUser.IsSuperuser) return null; if (currentUser.IsSuperuser) return null;
var allowed = await permission.HasPermissionAsync(new HasPermissionRequest var allowed = await permission.HasPermissionAsync(new HasPermissionRequest
{ Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" }); { Actor = currentUser.Id, Key = "files.create" });
return allowed.HasPermission return allowed.HasPermission
? null ? null

View File

@@ -1,118 +1,121 @@
{ {
"Debug": true, "Debug": true,
"BaseUrl": "http://localhost:5090", "BaseUrl": "http://localhost:5090",
"GatewayUrl": "http://localhost:5094", "GatewayUrl": "http://localhost:5094",
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" "App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
}, },
"Authentication": { "Authentication": {
"Schemes": { "Schemes": {
"Bearer": { "Bearer": {
"ValidAudiences": [ "ValidAudiences": [
"http://localhost:5071", "http://localhost:5071",
"https://localhost:7099" "https://localhost:7099"
], ],
"ValidIssuer": "solar-network" "ValidIssuer": "solar-network"
} }
} }
}, },
"AuthToken": { "AuthToken": {
"PublicKeyPath": "Keys/PublicKey.pem", "PublicKeyPath": "Keys/PublicKey.pem",
"PrivateKeyPath": "Keys/PrivateKey.pem" "PrivateKeyPath": "Keys/PrivateKey.pem"
}, },
"Storage": { "Storage": {
"Uploads": "Uploads", "Uploads": "Uploads",
"PreferredRemote": "c53136a6-9152-4ecb-9f88-43c41438c23e", "PreferredRemote": "c53136a6-9152-4ecb-9f88-43c41438c23e",
"Remote": [ "Remote": [
{ {
"Id": "minio", "Id": "minio",
"Label": "Minio", "Label": "Minio",
"Region": "auto", "Region": "auto",
"Bucket": "solar-network-development", "Bucket": "solar-network-development",
"Endpoint": "localhost:9000", "Endpoint": "localhost:9000",
"SecretId": "littlesheep", "SecretId": "littlesheep",
"SecretKey": "password", "SecretKey": "password",
"EnabledSigned": true, "EnabledSigned": true,
"EnableSsl": false "EnableSsl": false
}, },
{ {
"Id": "cloudflare", "Id": "cloudflare",
"Label": "Cloudflare R2", "Label": "Cloudflare R2",
"Region": "auto", "Region": "auto",
"Bucket": "solar-network", "Bucket": "solar-network",
"Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com", "Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com",
"SecretId": "8ff5d06c7b1639829d60bc6838a542e6", "SecretId": "8ff5d06c7b1639829d60bc6838a542e6",
"SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67", "SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67",
"EnableSigned": true, "EnableSigned": true,
"EnableSsl": true "EnableSsl": true
} }
]
},
"Captcha": {
"Provider": "cloudflare",
"ApiKey": "0x4AAAAAABCDUdOujj4feOb_",
"ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U"
},
"Notifications": {
"Topic": "dev.solsynth.solian",
"Endpoint": "http://localhost:8088"
},
"Email": {
"Server": "smtp4dev.orb.local",
"Port": 25,
"UseSsl": false,
"Username": "no-reply@mail.solsynth.dev",
"Password": "password",
"FromAddress": "no-reply@mail.solsynth.dev",
"FromName": "Alphabot",
"SubjectPrefix": "Solar Network"
},
"RealtimeChat": {
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
"ApiKey": "APIs6TiL8wj3A4j",
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
},
"GeoIp": {
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
},
"Oidc": {
"Google": {
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
"ClientSecret": ""
},
"Apple": {
"ClientId": "dev.solsynth.solian",
"TeamId": "W7HPZ53V6B",
"KeyId": "B668YP4KBG",
"PrivateKeyPath": "./Keys/Solarpass.p8"
},
"Microsoft": {
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
}
},
"Payment": {
"Auth": {
"Afdian": "<token here>"
},
"Subscriptions": {
"Afdian": {
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
}
}
},
"Cache": {
"Serializer": "MessagePack"
},
"KnownProxies": [
"127.0.0.1",
"::1"
] ]
},
"Captcha": {
"Provider": "cloudflare",
"ApiKey": "0x4AAAAAABCDUdOujj4feOb_",
"ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U"
},
"Notifications": {
"Topic": "dev.solsynth.solian",
"Endpoint": "http://localhost:8088"
},
"Email": {
"Server": "smtp4dev.orb.local",
"Port": 25,
"UseSsl": false,
"Username": "no-reply@mail.solsynth.dev",
"Password": "password",
"FromAddress": "no-reply@mail.solsynth.dev",
"FromName": "Alphabot",
"SubjectPrefix": "Solar Network"
},
"RealtimeChat": {
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
"ApiKey": "APIs6TiL8wj3A4j",
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
},
"GeoIp": {
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
},
"Oidc": {
"Google": {
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
"ClientSecret": ""
},
"Apple": {
"ClientId": "dev.solsynth.solian",
"TeamId": "W7HPZ53V6B",
"KeyId": "B668YP4KBG",
"PrivateKeyPath": "./Keys/Solarpass.p8"
},
"Microsoft": {
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
}
},
"Payment": {
"Auth": {
"Afdian": "<token here>"
},
"Subscriptions": {
"Afdian": {
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
}
}
},
"KnownProxies": [
"127.0.0.1",
"::1"
]
} }

View File

@@ -56,7 +56,7 @@ builder.Services.AddRateLimiter(options =>
}; };
}); });
var serviceNames = new[] { "ring", "pass", "drive", "sphere", "develop", "insight" }; var serviceNames = new[] { "ring", "pass", "drive", "sphere", "develop", "insight", "zone" };
var specialRoutes = new[] var specialRoutes = new[]
{ {

View File

@@ -1,13 +1,16 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
}
},
"Cache": {
"Serializer": "MessagePack"
},
"AllowedHosts": "*",
"SiteUrl": "http://localhost:3000",
"Client": {
"SomeSetting": "SomeValue"
} }
},
"AllowedHosts": "*",
"SiteUrl": "http://localhost:3000",
"Client": {
"SomeSetting": "SomeValue"
}
} }

View File

@@ -14,9 +14,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddAppServices(this IServiceCollection services) public static IServiceCollection AddAppServices(this IServiceCollection services)
{ {
services.AddDbContext<AppDatabase>(); services.AddDbContext<AppDatabase>();
services.AddSingleton<IClock>(SystemClock.Instance);
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddSingleton<ICacheService, CacheServiceRedis>();
services.AddHttpClient(); services.AddHttpClient();

View File

@@ -19,6 +19,9 @@
"Etcd": { "Etcd": {
"Insecure": true "Insecure": true
}, },
"Cache": {
"Serializer": "MessagePack"
},
"Thinking": { "Thinking": {
"DefaultService": "deepseek-chat", "DefaultService": "deepseek-chat",
"Services": { "Services": {

View File

@@ -1,9 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Affiliation;
using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Credit; using DysonNetwork.Pass.Credit;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Pass.Wallet; using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -22,7 +24,8 @@ public class AccountController(
SubscriptionService subscriptions, SubscriptionService subscriptions,
AccountEventService events, AccountEventService events,
SocialCreditService socialCreditService, SocialCreditService socialCreditService,
GeoIpService geo AffiliationSpellService ars,
GeoService geo
) : ControllerBase ) : ControllerBase
{ {
[HttpGet("{name}")] [HttpGet("{name}")]
@@ -34,7 +37,7 @@ public class AccountController(
.Include(e => e.Badges) .Include(e => e.Badges)
.Include(e => e.Profile) .Include(e => e.Profile)
.Include(e => e.Contacts.Where(c => c.IsPublic)) .Include(e => e.Contacts.Where(c => c.IsPublic))
.Where(a => a.Name == name) .Where(a => EF.Functions.Like(a.Name, name))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier)); if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
@@ -103,6 +106,52 @@ public class AccountController(
[MaxLength(32)] public string Language { get; set; } = "en-us"; [MaxLength(32)] public string Language { get; set; } = "en-us";
[Required] public string CaptchaToken { get; set; } = string.Empty; [Required] public string CaptchaToken { get; set; } = string.Empty;
public string? AffiliationSpell { get; set; }
}
public class AccountCreateValidateRequest
{
[MinLength(2)]
[MaxLength(256)]
[RegularExpression(@"^[A-Za-z0-9_-]+$",
ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
]
public string? Name { get; set; }
[EmailAddress]
[RegularExpression(@"^[^+]+@[^@]+\.[^@]+$", ErrorMessage = "Email address cannot contain '+' symbol.")]
[MaxLength(1024)]
public string? Email { get; set; }
public string? AffiliationSpell { get; set; }
}
[HttpPost("validate")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<string>> ValidateCreateAccountRequest(
[FromBody] AccountCreateValidateRequest request)
{
if (request.Name is not null)
{
if (await accounts.CheckAccountNameHasTaken(request.Name))
return BadRequest("Account name has already been taken.");
}
if (request.Email is not null)
{
if (await accounts.CheckEmailHasBeenUsed(request.Email))
return BadRequest("Email has already been used.");
}
if (request.AffiliationSpell is not null)
{
if (!await ars.CheckAffiliationSpellHasTaken(request.AffiliationSpell))
return BadRequest("No affiliation spell has been found.");
}
return Ok("Everything seems good.");
} }
[HttpPost] [HttpPost]
@@ -271,10 +320,21 @@ public class AccountController(
[HttpPost("credits/validate")] [HttpPost("credits/validate")]
[Authorize] [Authorize]
[RequiredPermission("maintenance", "credits.validate.perform")] [AskPermission("credits.validate.perform")]
public async Task<IActionResult> PerformSocialCreditValidation() public async Task<IActionResult> PerformSocialCreditValidation()
{ {
await socialCreditService.ValidateSocialCredits(); await socialCreditService.ValidateSocialCredits();
return Ok(); return Ok();
} }
}
[HttpDelete("{name}")]
[Authorize]
[AskPermission("accounts.deletion")]
public async Task<IActionResult> AdminDeleteAccount(string name)
{
var account = await accounts.LookupAccount(name);
if (account is null) return NotFound();
await accounts.DeleteAccount(account);
return Ok();
}
}

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Pass.Wallet; using DysonNetwork.Pass.Wallet;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
@@ -82,7 +83,7 @@ public class AccountCurrentController(
[MaxLength(4096)] public string? Bio { get; set; } [MaxLength(4096)] public string? Bio { get; set; }
public Shared.Models.UsernameColor? UsernameColor { get; set; } public Shared.Models.UsernameColor? UsernameColor { get; set; }
public Instant? Birthday { get; set; } public Instant? Birthday { get; set; }
public List<ProfileLink>? Links { get; set; } public List<SnProfileLink>? Links { get; set; }
[MaxLength(32)] public string? PictureId { get; set; } [MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; } [MaxLength(32)] public string? BackgroundId { get; set; }
@@ -194,7 +195,7 @@ public class AccountCurrentController(
} }
[HttpPatch("statuses")] [HttpPatch("statuses")]
[RequiredPermission("global", "accounts.statuses.update")] [AskPermission("accounts.statuses.update")]
public async Task<ActionResult<SnAccountStatus>> UpdateStatus([FromBody] AccountController.StatusRequest request) public async Task<ActionResult<SnAccountStatus>> UpdateStatus([FromBody] AccountController.StatusRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
@@ -228,7 +229,7 @@ public class AccountCurrentController(
} }
[HttpPost("statuses")] [HttpPost("statuses")]
[RequiredPermission("global", "accounts.statuses.create")] [AskPermission("accounts.statuses.create")]
public async Task<ActionResult<SnAccountStatus>> CreateStatus([FromBody] AccountController.StatusRequest request) public async Task<ActionResult<SnAccountStatus>> CreateStatus([FromBody] AccountController.StatusRequest request)
{ {
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
@@ -559,7 +560,7 @@ public class AccountCurrentController(
[HttpGet("devices")] [HttpGet("devices")]
[Authorize] [Authorize]
public async Task<ActionResult<List<SnAuthClientWithChallenge>>> GetDevices() public async Task<ActionResult<List<SnAuthClientWithSessions>>> GetDevices()
{ {
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser ||
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized();
@@ -570,18 +571,41 @@ public class AccountCurrentController(
.Where(device => device.AccountId == currentUser.Id) .Where(device => device.AccountId == currentUser.Id)
.ToListAsync(); .ToListAsync();
var challengeDevices = devices.Select(SnAuthClientWithChallenge.FromClient).ToList(); var sessionDevices = devices.ConvertAll(SnAuthClientWithSessions.FromClient).ToList();
var deviceIds = challengeDevices.Select(x => x.Id).ToList(); var clientIds = sessionDevices.Select(x => x.Id).ToList();
var authChallenges = await db.AuthChallenges var authSessions = await db.AuthSessions
.Where(c => c.ClientId != null && deviceIds.Contains(c.ClientId.Value)) .Where(c => c.ClientId != null && clientIds.Contains(c.ClientId.Value))
.GroupBy(c => c.ClientId) .GroupBy(c => c.ClientId!.Value)
.ToDictionaryAsync(c => c.Key!.Value, c => c.ToList()); .ToDictionaryAsync(c => c.Key, c => c.ToList());
foreach (var challengeDevice in challengeDevices) foreach (var dev in sessionDevices)
if (authChallenges.TryGetValue(challengeDevice.Id, out var challenge)) if (authSessions.TryGetValue(dev.Id, out var challenge))
challengeDevice.Challenges = challenge; dev.Sessions = challenge;
return Ok(challengeDevices); return Ok(sessionDevices);
}
[HttpGet("challenges")]
[Authorize]
public async Task<ActionResult<List<SnAuthChallenge>>> GetChallenges(
[FromQuery] int take = 20,
[FromQuery] int offset = 0
)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
var query = db.AuthChallenges
.Where(challenge => challenge.AccountId == currentUser.Id)
.OrderByDescending(c => c.CreatedAt);
var total = await query.CountAsync();
Response.Headers.Append("X-Total", total.ToString());
var challenges = await query
.Skip(offset)
.Take(take)
.ToListAsync();
return Ok(challenges);
} }
[HttpGet("sessions")] [HttpGet("sessions")]
@@ -595,8 +619,8 @@ public class AccountCurrentController(
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized();
var query = db.AuthSessions var query = db.AuthSessions
.OrderByDescending(x => x.LastGrantedAt)
.Include(session => session.Account) .Include(session => session.Account)
.Include(session => session.Challenge)
.Where(session => session.Account.Id == currentUser.Id); .Where(session => session.Account.Id == currentUser.Id);
var total = await query.CountAsync(); var total = await query.CountAsync();
@@ -604,7 +628,6 @@ public class AccountCurrentController(
Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString()); Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
var sessions = await query var sessions = await query
.OrderByDescending(x => x.LastGrantedAt)
.Skip(offset) .Skip(offset)
.Take(take) .Take(take)
.ToListAsync(); .ToListAsync();
@@ -688,7 +711,7 @@ public class AccountCurrentController(
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser || if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser ||
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized();
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.Challenge.ClientId); var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.ClientId);
if (device is null) return NotFound(); if (device is null) return NotFound();
try try

View File

@@ -313,52 +313,84 @@ public class AccountEventService(
CultureInfo.CurrentCulture = cultureInfo; CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo; CultureInfo.CurrentUICulture = cultureInfo;
// Generate 2 positive tips var accountProfile = await db.AccountProfiles
var positiveIndices = Enumerable.Range(1, FortuneTipCount)
.OrderBy(_ => Random.Next())
.Take(2)
.ToList();
var tips = positiveIndices.Select(index => new CheckInFortuneTip
{
IsPositive = true,
Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value,
Content = localizer[$"FortuneTipPositiveContent_{index}"].Value
}).ToList();
// Generate 2 negative tips
var negativeIndices = Enumerable.Range(1, FortuneTipCount)
.Except(positiveIndices)
.OrderBy(_ => Random.Next())
.Take(2)
.ToList();
tips.AddRange(negativeIndices.Select(index => new CheckInFortuneTip
{
IsPositive = false,
Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value,
Content = localizer[$"FortuneTipNegativeContent_{index}"].Value
}));
// The 5 is specialized, keep it alone.
// Use weighted random distribution to make all levels reasonably achievable
// Weights: Worst: 10%, Worse: 20%, Normal: 40%, Better: 20%, Best: 10%
var randomValue = Random.Next(100);
var checkInLevel = randomValue switch
{
< 10 => CheckInResultLevel.Worst, // 0-9: 10% chance
< 30 => CheckInResultLevel.Worse, // 10-29: 20% chance
< 70 => CheckInResultLevel.Normal, // 30-69: 40% chance
< 90 => CheckInResultLevel.Better, // 70-89: 20% chance
_ => CheckInResultLevel.Best // 90-99: 10% chance
};
var accountBirthday = await db.AccountProfiles
.Where(x => x.AccountId == user.Id) .Where(x => x.AccountId == user.Id)
.Select(x => x.Birthday) .Select(x => new { x.Birthday, x.TimeZone })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var accountBirthday = accountProfile?.Birthday;
var now = SystemClock.Instance.GetCurrentInstant().InUtc().Date; var now = SystemClock.Instance.GetCurrentInstant();
if (accountBirthday.HasValue && accountBirthday.Value.InUtc().Date == now)
var userTimeZone = DateTimeZone.Utc;
if (!string.IsNullOrEmpty(accountProfile?.TimeZone))
{
userTimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(accountProfile.TimeZone) ?? DateTimeZone.Utc;
}
var todayInUserTz = now.InZone(userTimeZone).Date;
var birthdayDate = accountBirthday?.InZone(userTimeZone).Date;
var isBirthday = birthdayDate.HasValue &&
birthdayDate.Value.Month == todayInUserTz.Month &&
birthdayDate.Value.Day == todayInUserTz.Day;
List<CheckInFortuneTip> tips;
CheckInResultLevel checkInLevel;
if (isBirthday)
{
// Skip random logic and tips generation for birthday
checkInLevel = CheckInResultLevel.Special; checkInLevel = CheckInResultLevel.Special;
tips = [
new CheckInFortuneTip()
{
IsPositive = true,
Title = localizer["FortuneTipSpecialTitle_Birthday"].Value,
Content = localizer["FortuneTipSpecialContent_Birthday", user.Nick].Value,
}
];
}
else
{
// Generate 2 positive tips
var positiveIndices = Enumerable.Range(1, FortuneTipCount)
.OrderBy(_ => Random.Next())
.Take(2)
.ToList();
tips = positiveIndices.Select(index => new CheckInFortuneTip
{
IsPositive = true,
Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value,
Content = localizer[$"FortuneTipPositiveContent_{index}"].Value
}).ToList();
// Generate 2 negative tips
var negativeIndices = Enumerable.Range(1, FortuneTipCount)
.Except(positiveIndices)
.OrderBy(_ => Random.Next())
.Take(2)
.ToList();
tips.AddRange(negativeIndices.Select(index => new CheckInFortuneTip
{
IsPositive = false,
Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value,
Content = localizer[$"FortuneTipNegativeContent_{index}"].Value
}));
// The 5 is specialized, keep it alone.
// Use weighted random distribution to make all levels reasonably achievable
// Weights: Worst: 10%, Worse: 20%, Normal: 40%, Better: 20%, Best: 10%
var randomValue = Random.Next(100);
checkInLevel = randomValue switch
{
< 10 => CheckInResultLevel.Worst, // 0-9: 10% chance
< 30 => CheckInResultLevel.Worse, // 10-29: 20% chance
< 70 => CheckInResultLevel.Normal, // 30-69: 40% chance
< 90 => CheckInResultLevel.Better, // 70-89: 20% chance
_ => CheckInResultLevel.Best // 90-99: 10% chance
};
}
var result = new SnCheckInResult var result = new SnCheckInResult
{ {

View File

@@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using DysonNetwork.Pass.Affiliation;
using DysonNetwork.Pass.Auth.OpenId; using DysonNetwork.Pass.Auth.OpenId;
using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Localization;
using DysonNetwork.Pass.Mailer; using DysonNetwork.Pass.Mailer;
@@ -24,6 +25,7 @@ public class AccountService(
FileService.FileServiceClient files, FileService.FileServiceClient files,
FileReferenceService.FileReferenceServiceClient fileRefs, FileReferenceService.FileReferenceServiceClient fileRefs,
AccountUsernameService uname, AccountUsernameService uname,
AffiliationSpellService ars,
EmailService mailer, EmailService mailer,
RingService.RingServiceClient pusher, RingService.RingServiceClient pusher,
IStringLocalizer<NotificationResource> localizer, IStringLocalizer<NotificationResource> localizer,
@@ -54,11 +56,13 @@ public class AccountService(
public async Task<SnAccount?> LookupAccount(string probe) public async Task<SnAccount?> LookupAccount(string probe)
{ {
var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync(); var account = await db.Accounts.Where(a => EF.Functions.ILike(a.Name, probe)).FirstOrDefaultAsync();
if (account is not null) return account; if (account is not null) return account;
var contact = await db.AccountContacts var contact = await db.AccountContacts
.Where(c => c.Content == probe) .Where(c => c.Type == Shared.Models.AccountContactType.Email ||
c.Type == Shared.Models.AccountContactType.PhoneNumber)
.Where(c => EF.Functions.ILike(c.Content, probe))
.Include(c => c.Account) .Include(c => c.Account)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
return contact?.Account; return contact?.Account;
@@ -81,6 +85,17 @@ public class AccountService(
return profile?.Level; return profile?.Level;
} }
public async Task<bool> CheckAccountNameHasTaken(string name)
{
return await db.Accounts.AnyAsync(a => EF.Functions.ILike(a.Name, name));
}
public async Task<bool> CheckEmailHasBeenUsed(string email)
{
return await db.AccountContacts.AnyAsync(c =>
c.Type == Shared.Models.AccountContactType.Email && EF.Functions.ILike(c.Content, email));
}
public async Task<SnAccount> CreateAccount( public async Task<SnAccount> CreateAccount(
string name, string name,
string nick, string nick,
@@ -88,12 +103,12 @@ public class AccountService(
string? password, string? password,
string language = "en-US", string language = "en-US",
string region = "en", string region = "en",
string? affiliationSpell = null,
bool isEmailVerified = false, bool isEmailVerified = false,
bool isActivated = false bool isActivated = false
) )
{ {
var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync(); if (await CheckAccountNameHasTaken(name))
if (dupeNameCount > 0)
throw new InvalidOperationException("Account name has already been taken."); throw new InvalidOperationException("Account name has already been taken.");
var dupeEmailCount = await db.AccountContacts var dupeEmailCount = await db.AccountContacts
@@ -101,7 +116,7 @@ public class AccountService(
).CountAsync(); ).CountAsync();
if (dupeEmailCount > 0) if (dupeEmailCount > 0)
throw new InvalidOperationException("Account email has already been used."); throw new InvalidOperationException("Account email has already been used.");
var account = new SnAccount var account = new SnAccount
{ {
Name = name, Name = name,
@@ -110,7 +125,7 @@ public class AccountService(
Region = region, Region = region,
Contacts = Contacts =
[ [
new() new SnAccountContact
{ {
Type = Shared.Models.AccountContactType.Email, Type = Shared.Models.AccountContactType.Email,
Content = email, Content = email,
@@ -132,6 +147,9 @@ public class AccountService(
Profile = new SnAccountProfile() Profile = new SnAccountProfile()
}; };
if (affiliationSpell is not null)
await ars.CreateAffiliationResult(affiliationSpell, $"account:{account.Id}");
if (isActivated) if (isActivated)
{ {
account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
@@ -140,7 +158,7 @@ public class AccountService(
{ {
db.PermissionGroupMembers.Add(new SnPermissionGroupMember db.PermissionGroupMembers.Add(new SnPermissionGroupMember
{ {
Actor = $"user:{account.Id}", Actor = account.Id.ToString(),
Group = defaultGroup Group = defaultGroup
}); });
} }
@@ -181,10 +199,7 @@ public class AccountService(
displayName, displayName,
userInfo.Email, userInfo.Email,
null, null,
"en-US", isEmailVerified: userInfo.EmailVerified
"en",
userInfo.EmailVerified,
userInfo.EmailVerified
); );
} }
@@ -274,7 +289,8 @@ public class AccountService(
return isExists; return isExists;
} }
public async Task<SnAccountAuthFactor?> CreateAuthFactor(SnAccount account, Shared.Models.AccountAuthFactorType type, string? secret) public async Task<SnAccountAuthFactor?> CreateAuthFactor(SnAccount account,
Shared.Models.AccountAuthFactorType type, string? secret)
{ {
SnAccountAuthFactor? factor = null; SnAccountAuthFactor? factor = null;
switch (type) switch (type)
@@ -352,7 +368,8 @@ public class AccountService(
public async Task<SnAccountAuthFactor> EnableAuthFactor(SnAccountAuthFactor factor, string? code) public async Task<SnAccountAuthFactor> EnableAuthFactor(SnAccountAuthFactor factor, string? code)
{ {
if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled."); if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled.");
if (factor.Type is Shared.Models.AccountAuthFactorType.Password or Shared.Models.AccountAuthFactorType.TimedCode) if (factor.Type is Shared.Models.AccountAuthFactorType.Password
or Shared.Models.AccountAuthFactorType.TimedCode)
{ {
if (code is null || !factor.VerifyPassword(code)) if (code is null || !factor.VerifyPassword(code))
throw new InvalidOperationException( throw new InvalidOperationException(
@@ -508,9 +525,7 @@ public class AccountService(
private async Task<bool> IsDeviceActive(Guid id) private async Task<bool> IsDeviceActive(Guid id)
{ {
return await db.AuthSessions return await db.AuthSessions.AnyAsync(s => s.ClientId == id);
.Include(s => s.Challenge)
.AnyAsync(s => s.Challenge.ClientId == id);
} }
public async Task<SnAuthClient> UpdateDeviceName(SnAccount account, string deviceId, string label) public async Task<SnAuthClient> UpdateDeviceName(SnAccount account, string deviceId, string label)
@@ -529,8 +544,7 @@ public class AccountService(
public async Task DeleteSession(SnAccount account, Guid sessionId) public async Task DeleteSession(SnAccount account, Guid sessionId)
{ {
var session = await db.AuthSessions var session = await db.AuthSessions
.Include(s => s.Challenge) .Include(s => s.Client)
.ThenInclude(s => s.Client)
.Where(s => s.Id == sessionId && s.AccountId == account.Id) .Where(s => s.Id == sessionId && s.AccountId == account.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (session is null) throw new InvalidOperationException("Session was not found."); if (session is null) throw new InvalidOperationException("Session was not found.");
@@ -539,11 +553,11 @@ public class AccountService(
db.AuthSessions.Remove(session); db.AuthSessions.Remove(session);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
if (session.Challenge.ClientId.HasValue) if (session.ClientId.HasValue)
{ {
if (!await IsDeviceActive(session.Challenge.ClientId.Value)) if (!await IsDeviceActive(session.ClientId.Value))
await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest() await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
{ DeviceId = session.Challenge.Client!.DeviceId } { DeviceId = session.Client!.DeviceId }
); );
} }
@@ -564,15 +578,13 @@ public class AccountService(
); );
var sessions = await db.AuthSessions var sessions = await db.AuthSessions
.Include(s => s.Challenge) .Where(s => s.ClientId == device.Id && s.AccountId == account.Id)
.Where(s => s.Challenge.ClientId == device.Id && s.AccountId == account.Id)
.ToListAsync(); .ToListAsync();
// The current session should be included in the sessions' list // The current session should be included in the sessions' list
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
await db.AuthSessions await db.AuthSessions
.Include(s => s.Challenge) .Where(s => s.ClientId == device.Id)
.Where(s => s.Challenge.ClientId == device.Id)
.ExecuteUpdateAsync(p => p.SetProperty(s => s.DeletedAt, s => now)); .ExecuteUpdateAsync(p => p.SetProperty(s => s.DeletedAt, s => now));
db.AuthClients.Remove(device); db.AuthClients.Remove(device);
@@ -582,7 +594,8 @@ public class AccountService(
await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{item.Id}"); await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{item.Id}");
} }
public async Task<SnAccountContact> CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type, string content) public async Task<SnAccountContact> CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type,
string content)
{ {
var isExists = await db.AccountContacts var isExists = await db.AccountContacts
.Where(x => x.AccountId == account.Id && x.Type == type && x.Content == content) .Where(x => x.AccountId == account.Id && x.Type == type && x.Content == content)
@@ -644,7 +657,8 @@ public class AccountService(
} }
} }
public async Task<SnAccountContact> SetContactMethodPublic(SnAccount account, SnAccountContact contact, bool isPublic) public async Task<SnAccountContact> SetContactMethodPublic(SnAccount account, SnAccountContact contact,
bool isPublic)
{ {
contact.IsPublic = isPublic; contact.IsPublic = isPublic;
db.AccountContacts.Update(contact); db.AccountContacts.Update(contact);

View File

@@ -24,15 +24,16 @@ public class AccountServiceGrpc(
public override async Task<Shared.Proto.Account> GetAccount(GetAccountRequest request, ServerCallContext context) public override async Task<Shared.Proto.Account> GetAccount(GetAccountRequest request, ServerCallContext context)
{ {
if (!Guid.TryParse(request.Id, out var accountId)) if (!Guid.TryParse(request.Id, out var accountId))
throw new RpcException(new Grpc.Core.Status(StatusCode.InvalidArgument, "Invalid account ID format")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid account ID format"));
var account = await _db.Accounts var account = await _db.Accounts
.AsNoTracking() .AsNoTracking()
.Include(a => a.Profile) .Include(a => a.Profile)
.Include(a => a.Contacts.Where(c => c.IsPublic))
.FirstOrDefaultAsync(a => a.Id == accountId); .FirstOrDefaultAsync(a => a.Id == accountId);
if (account == null) if (account == null)
throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account {request.Id} not found")); throw new RpcException(new Status(StatusCode.NotFound, $"Account {request.Id} not found"));
var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id); var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
account.PerkSubscription = perk?.ToReference(); account.PerkSubscription = perk?.ToReference();

View File

@@ -1,10 +1,10 @@
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
namespace DysonNetwork.Pass.Account; namespace DysonNetwork.Pass.Account;
public class ActionLogService(GeoIpService geo, FlushBufferService fbs) public class ActionLogService(GeoService geo, FlushBufferService fbs)
{ {
public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta) public void CreateActionLog(Guid accountId, string action, Dictionary<string, object> meta)
{ {

View File

@@ -1,3 +1,5 @@
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -7,17 +9,31 @@ namespace DysonNetwork.Pass.Account;
[Route("/api/spells")] [Route("/api/spells")]
public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase
{ {
[HttpPost("activation/resend")]
[Authorize]
public async Task<ActionResult> ResendActivationMagicSpell()
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
var spell = await db.MagicSpells.FirstOrDefaultAsync(s =>
s.Type == MagicSpellType.AccountActivation && s.AccountId == currentUser.Id);
if (spell is null) return BadRequest("Unable to find activation magic spell.");
await sp.NotifyMagicSpell(spell, true);
return Ok();
}
[HttpPost("{spellId:guid}/resend")] [HttpPost("{spellId:guid}/resend")]
public async Task<ActionResult> ResendMagicSpell(Guid spellId) public async Task<ActionResult> ResendMagicSpell(Guid spellId)
{ {
var spell = db.MagicSpells.FirstOrDefault(x => x.Id == spellId); var spell = db.MagicSpells.FirstOrDefault(x => x.Id == spellId);
if (spell == null) if (spell == null)
return NotFound(); return NotFound();
await sp.NotifyMagicSpell(spell, true); await sp.NotifyMagicSpell(spell, true);
return Ok(); return Ok();
} }
[HttpGet("{spellWord}")] [HttpGet("{spellWord}")]
public async Task<ActionResult> GetMagicSpell(string spellWord) public async Task<ActionResult> GetMagicSpell(string spellWord)
{ {
@@ -38,7 +54,8 @@ public class MagicSpellController(AppDatabase db, MagicSpellService sp) : Contro
} }
[HttpPost("{spellWord}/apply")] [HttpPost("{spellWord}/apply")]
public async Task<ActionResult> ApplyMagicSpell([FromRoute] string spellWord, [FromBody] MagicSpellApplyRequest? request) public async Task<ActionResult> ApplyMagicSpell([FromRoute] string spellWord,
[FromBody] MagicSpellApplyRequest? request)
{ {
var word = Uri.UnescapeDataString(spellWord); var word = Uri.UnescapeDataString(spellWord);
var spell = await db.MagicSpells var spell = await db.MagicSpells
@@ -59,6 +76,7 @@ public class MagicSpellController(AppDatabase db, MagicSpellService sp) : Contro
{ {
return BadRequest(ex.Message); return BadRequest(ex.Message);
} }
return Ok(); return Ok();
} }
} }

View File

@@ -26,6 +26,7 @@ public class MagicSpellService(
Dictionary<string, object> meta, Dictionary<string, object> meta,
Instant? expiredAt = null, Instant? expiredAt = null,
Instant? affectedAt = null, Instant? affectedAt = null,
string? code = null,
bool preventRepeat = false bool preventRepeat = false
) )
{ {
@@ -41,7 +42,7 @@ public class MagicSpellService(
return existingSpell; return existingSpell;
} }
var spellWord = _GenerateRandomString(128); var spellWord = code ?? _GenerateRandomString(128);
var spell = new SnMagicSpell var spell = new SnMagicSpell
{ {
Spell = spellWord, Spell = spellWord,
@@ -193,7 +194,7 @@ public class MagicSpellService(
{ {
db.PermissionGroupMembers.Add(new SnPermissionGroupMember db.PermissionGroupMembers.Add(new SnPermissionGroupMember
{ {
Actor = $"user:{account.Id}", Actor = account.Id.ToString(),
Group = defaultGroup Group = defaultGroup
}); });
} }

View File

@@ -17,12 +17,18 @@ public class RelationshipService(
{ {
private const string UserFriendsCacheKeyPrefix = "accounts:friends:"; private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
private const string UserBlockedCacheKeyPrefix = "accounts:blocked:"; private const string UserBlockedCacheKeyPrefix = "accounts:blocked:";
private static readonly TimeSpan CacheExpiration = TimeSpan.FromHours(1);
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId) public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
{ {
if (accountId == Guid.Empty || relatedId == Guid.Empty)
throw new ArgumentException("Account IDs cannot be empty.");
if (accountId == relatedId)
return false; // Prevent self-relationships
var count = await db.AccountRelationships var count = await db.AccountRelationships
.Where(r => (r.AccountId == accountId && r.RelatedId == relatedId) || .Where(r => (r.AccountId == accountId && r.RelatedId == relatedId) ||
(r.AccountId == relatedId && r.AccountId == accountId)) (r.AccountId == relatedId && r.RelatedId == accountId))
.CountAsync(); .CountAsync();
return count > 0; return count > 0;
} }
@@ -34,6 +40,9 @@ public class RelationshipService(
bool ignoreExpired = false bool ignoreExpired = false
) )
{ {
if (accountId == Guid.Empty || relatedId == Guid.Empty)
throw new ArgumentException("Account IDs cannot be empty.");
var now = Instant.FromDateTimeUtc(DateTime.UtcNow); var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
var queries = db.AccountRelationships.AsQueryable() var queries = db.AccountRelationships.AsQueryable()
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId); .Where(r => r.AccountId == accountId && r.RelatedId == relatedId);
@@ -61,7 +70,7 @@ public class RelationshipService(
db.AccountRelationships.Add(relationship); db.AccountRelationships.Add(relationship);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await PurgeRelationshipCache(sender.Id, target.Id); await PurgeRelationshipCache(sender.Id, target.Id, status);
return relationship; return relationship;
} }
@@ -80,7 +89,7 @@ public class RelationshipService(
db.Remove(relationship); db.Remove(relationship);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await PurgeRelationshipCache(sender.Id, target.Id); await PurgeRelationshipCache(sender.Id, target.Id, RelationshipStatus.Blocked);
return relationship; return relationship;
} }
@@ -114,19 +123,24 @@ public class RelationshipService(
} }
}); });
await PurgeRelationshipCache(sender.Id, target.Id, RelationshipStatus.Pending);
return relationship; return relationship;
} }
public async Task DeleteFriendRequest(Guid accountId, Guid relatedId) public async Task DeleteFriendRequest(Guid accountId, Guid relatedId)
{ {
var relationship = await GetRelationship(accountId, relatedId, RelationshipStatus.Pending); if (accountId == Guid.Empty || relatedId == Guid.Empty)
if (relationship is null) throw new ArgumentException("Friend request was not found."); throw new ArgumentException("Account IDs cannot be empty.");
await db.AccountRelationships var affectedRows = await db.AccountRelationships
.Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending) .Where(r => r.AccountId == accountId && r.RelatedId == relatedId && r.Status == RelationshipStatus.Pending)
.ExecuteDeleteAsync(); .ExecuteDeleteAsync();
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); if (affectedRows == 0)
throw new ArgumentException("Friend request was not found.");
await PurgeRelationshipCache(accountId, relatedId, RelationshipStatus.Pending);
} }
public async Task<SnAccountRelationship> AcceptFriendRelationship( public async Task<SnAccountRelationship> AcceptFriendRelationship(
@@ -155,7 +169,7 @@ public class RelationshipService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId); await PurgeRelationshipCache(relationship.AccountId, relationship.RelatedId, RelationshipStatus.Friends, status);
return relationshipBackward; return relationshipBackward;
} }
@@ -165,11 +179,12 @@ public class RelationshipService(
var relationship = await GetRelationship(accountId, relatedId); var relationship = await GetRelationship(accountId, relatedId);
if (relationship is null) throw new ArgumentException("There is no relationship between you and the user."); if (relationship is null) throw new ArgumentException("There is no relationship between you and the user.");
if (relationship.Status == status) return relationship; if (relationship.Status == status) return relationship;
var oldStatus = relationship.Status;
relationship.Status = status; relationship.Status = status;
db.Update(relationship); db.Update(relationship);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await PurgeRelationshipCache(accountId, relatedId); await PurgeRelationshipCache(accountId, relatedId, oldStatus, status);
return relationship; return relationship;
} }
@@ -181,21 +196,7 @@ public class RelationshipService(
public async Task<List<Guid>> ListAccountFriends(Guid accountId) public async Task<List<Guid>> ListAccountFriends(Guid accountId)
{ {
var cacheKey = $"{UserFriendsCacheKeyPrefix}{accountId}"; return await GetCachedRelationships(accountId, RelationshipStatus.Friends, UserFriendsCacheKeyPrefix);
var friends = await cache.GetAsync<List<Guid>>(cacheKey);
if (friends == null)
{
friends = await db.AccountRelationships
.Where(r => r.RelatedId == accountId)
.Where(r => r.Status == RelationshipStatus.Friends)
.Select(r => r.AccountId)
.ToListAsync();
await cache.SetAsync(cacheKey, friends, TimeSpan.FromHours(1));
}
return friends ?? [];
} }
public async Task<List<Guid>> ListAccountBlocked(SnAccount account) public async Task<List<Guid>> ListAccountBlocked(SnAccount account)
@@ -205,21 +206,7 @@ public class RelationshipService(
public async Task<List<Guid>> ListAccountBlocked(Guid accountId) public async Task<List<Guid>> ListAccountBlocked(Guid accountId)
{ {
var cacheKey = $"{UserBlockedCacheKeyPrefix}{accountId}"; return await GetCachedRelationships(accountId, RelationshipStatus.Blocked, UserBlockedCacheKeyPrefix);
var blocked = await cache.GetAsync<List<Guid>>(cacheKey);
if (blocked == null)
{
blocked = await db.AccountRelationships
.Where(r => r.RelatedId == accountId)
.Where(r => r.Status == RelationshipStatus.Blocked)
.Select(r => r.AccountId)
.ToListAsync();
await cache.SetAsync(cacheKey, blocked, TimeSpan.FromHours(1));
}
return blocked ?? [];
} }
public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
@@ -229,11 +216,52 @@ public class RelationshipService(
return relationship is not null; return relationship is not null;
} }
private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId) private async Task<List<Guid>> GetCachedRelationships(Guid accountId, RelationshipStatus status, string cachePrefix)
{ {
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{accountId}"); if (accountId == Guid.Empty)
await cache.RemoveAsync($"{UserFriendsCacheKeyPrefix}{relatedId}"); throw new ArgumentException("Account ID cannot be empty.");
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{accountId}");
await cache.RemoveAsync($"{UserBlockedCacheKeyPrefix}{relatedId}"); var cacheKey = $"{cachePrefix}{accountId}";
var relationships = await cache.GetAsync<List<Guid>>(cacheKey);
if (relationships == null)
{
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
relationships = await db.AccountRelationships
.Where(r => r.RelatedId == accountId)
.Where(r => r.Status == status)
.Where(r => r.ExpiredAt == null || r.ExpiredAt > now)
.Select(r => r.AccountId)
.ToListAsync();
await cache.SetAsync(cacheKey, relationships, CacheExpiration);
}
return relationships ?? new List<Guid>();
} }
}
private async Task PurgeRelationshipCache(Guid accountId, Guid relatedId, params RelationshipStatus[] statuses)
{
if (statuses.Length == 0)
{
statuses = Enum.GetValues<RelationshipStatus>();
}
var keysToRemove = new List<string>();
if (statuses.Contains(RelationshipStatus.Friends) || statuses.Contains(RelationshipStatus.Pending))
{
keysToRemove.Add($"{UserFriendsCacheKeyPrefix}{accountId}");
keysToRemove.Add($"{UserFriendsCacheKeyPrefix}{relatedId}");
}
if (statuses.Contains(RelationshipStatus.Blocked))
{
keysToRemove.Add($"{UserBlockedCacheKeyPrefix}{accountId}");
keysToRemove.Add($"{UserBlockedCacheKeyPrefix}{relatedId}");
}
var removeTasks = keysToRemove.Select(key => cache.RemoveAsync(key));
await Task.WhenAll(removeTasks);
}
}

View File

@@ -0,0 +1,134 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Pass.Affiliation;
[ApiController]
[Route("/api/affiliations")]
public class AffiliationSpellController(AppDatabase db, AffiliationSpellService ars) : ControllerBase
{
public class CreateAffiliationSpellRequest
{
[MaxLength(1024)] public string? Spell { get; set; }
}
[HttpPost]
[Authorize]
public async Task<ActionResult<SnAffiliationSpell>> CreateSpell([FromBody] CreateAffiliationSpellRequest request)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
try
{
var spell = await ars.CreateAffiliationSpell(currentUser.Id, request.Spell);
return Ok(spell);
}
catch (InvalidOperationException e)
{
return BadRequest(e.Message);
}
}
[HttpGet]
[Authorize]
public async Task<ActionResult<List<SnAffiliationSpell>>> ListCreatedSpells(
[FromQuery(Name = "order")] string orderBy = "date",
[FromQuery(Name = "desc")] bool orderDesc = false,
[FromQuery] int take = 10,
[FromQuery] int offset = 0
)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
var queryable = db.AffiliationSpells
.Where(s => s.AccountId == currentUser.Id)
.AsQueryable();
queryable = orderBy switch
{
"usage" => orderDesc
? queryable.OrderByDescending(q => q.Results.Count)
: queryable.OrderBy(q => q.Results.Count),
_ => orderDesc
? queryable.OrderByDescending(q => q.CreatedAt)
: queryable.OrderBy(q => q.CreatedAt)
};
var totalCount = queryable.Count();
Response.Headers["X-Total"] = totalCount.ToString();
var spells = await queryable
.Skip(offset)
.Take(take)
.ToListAsync();
return Ok(spells);
}
[HttpGet("{id:guid}")]
[Authorize]
public async Task<ActionResult<SnAffiliationSpell>> GetSpell([FromRoute] Guid id)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
var spell = await db.AffiliationSpells
.Where(s => s.AccountId == currentUser.Id)
.Where(s => s.Id == id)
.FirstOrDefaultAsync();
if (spell is null) return NotFound();
return Ok(spell);
}
[HttpGet("{id:guid}/results")]
[Authorize]
public async Task<ActionResult<List<SnAffiliationResult>>> ListResults(
[FromRoute] Guid id,
[FromQuery(Name = "desc")] bool orderDesc = false,
[FromQuery] int take = 10,
[FromQuery] int offset = 0
)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
var queryable = db.AffiliationResults
.Include(r => r.Spell)
.Where(r => r.Spell.AccountId == currentUser.Id)
.Where(r => r.SpellId == id)
.AsQueryable();
// Order by creation date
queryable = orderDesc
? queryable.OrderByDescending(r => r.CreatedAt)
: queryable.OrderBy(r => r.CreatedAt);
var totalCount = queryable.Count();
Response.Headers["X-Total"] = totalCount.ToString();
var results = await queryable
.Skip(offset)
.Take(take)
.ToListAsync();
return Ok(results);
}
[HttpDelete("{id:guid}")]
[Authorize]
public async Task<ActionResult> DeleteSpell([FromRoute] Guid id)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
var spell = await db.AffiliationSpells
.Where(s => s.AccountId == currentUser.Id)
.Where(s => s.Id == id)
.FirstOrDefaultAsync();
if (spell is null) return NotFound();
db.AffiliationSpells.Remove(spell);
await db.SaveChangesAsync();
return Ok();
}
}

View File

@@ -0,0 +1,62 @@
using System.Security.Cryptography;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Pass.Affiliation;
public class AffiliationSpellService(AppDatabase db)
{
public async Task<SnAffiliationSpell> CreateAffiliationSpell(Guid accountId, string? spellWord)
{
spellWord ??= _GenerateRandomString(8);
if (await CheckAffiliationSpellHasTaken(spellWord))
throw new InvalidOperationException("The spell has been taken.");
var spell = new SnAffiliationSpell
{
AccountId = accountId,
Spell = spellWord
};
db.AffiliationSpells.Add(spell);
await db.SaveChangesAsync();
return spell;
}
public async Task<SnAffiliationResult> CreateAffiliationResult(string spellWord, string resourceId)
{
var spell =
await db.AffiliationSpells.FirstOrDefaultAsync(a => a.Spell == spellWord);
if (spell is null) throw new InvalidOperationException("The spell was not found.");
var result = new SnAffiliationResult
{
Spell = spell,
ResourceIdentifier = resourceId
};
db.AffiliationResults.Add(result);
await db.SaveChangesAsync();
return result;
}
public async Task<bool> CheckAffiliationSpellHasTaken(string spellWord)
{
return await db.AffiliationSpells.AnyAsync(s => s.Spell == spellWord);
}
private static string _GenerateRandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var result = new char[length];
using var rng = RandomNumberGenerator.Create();
for (var i = 0; i < length; i++)
{
var bytes = new byte[1];
rng.GetBytes(bytes);
result[i] = chars[bytes[0] % chars.Length];
}
return new string(result);
}
}

View File

@@ -61,6 +61,9 @@ public class AppDatabase(
public DbSet<SnLottery> Lotteries { get; set; } = null!; public DbSet<SnLottery> Lotteries { get; set; } = null!;
public DbSet<SnLotteryRecord> LotteryRecords { get; set; } = null!; public DbSet<SnLotteryRecord> LotteryRecords { get; set; } = null!;
public DbSet<SnAffiliationSpell> AffiliationSpells { get; set; } = null!;
public DbSet<SnAffiliationResult> AffiliationResults { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseNpgsql( optionsBuilder.UseNpgsql(
@@ -100,7 +103,7 @@ public class AppDatabase(
"stickers.packs.create", "stickers.packs.create",
"stickers.create" "stickers.create"
}.Select(permission => }.Select(permission =>
PermissionService.NewPermissionNode("group:default", "global", permission, true)) PermissionService.NewPermissionNode("group:default", permission, true))
.ToList() .ToList()
}); });
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);

View File

@@ -70,7 +70,7 @@ public class DysonTokenAuthHandler(
}; };
// Add scopes as claims // Add scopes as claims
session.Challenge?.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope))); session.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope)));
// Add superuser claim if applicable // Add superuser claim if applicable
if (session.Account.IsSuperuser) if (session.Account.IsSuperuser)
@@ -117,16 +117,17 @@ public class DysonTokenAuthHandler(
{ {
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{ {
var token = authHeader["Bearer ".Length..].Trim(); var tokenText = authHeader["Bearer ".Length..].Trim();
var parts = token.Split('.'); var parts = tokenText.Split('.');
return new TokenInfo return new TokenInfo
{ {
Token = token, Token = tokenText,
Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey
}; };
} }
else if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
{ {
return new TokenInfo return new TokenInfo
{ {
@@ -134,7 +135,8 @@ public class DysonTokenAuthHandler(
Type = TokenType.AuthKey Type = TokenType.AuthKey
}; };
} }
else if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
{ {
return new TokenInfo return new TokenInfo
{ {

View File

@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using NodaTime; using NodaTime;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Localization;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using AccountService = DysonNetwork.Pass.Account.AccountService; using AccountService = DysonNetwork.Pass.Account.AccountService;
@@ -18,7 +18,7 @@ public class AuthController(
AppDatabase db, AppDatabase db,
AccountService accounts, AccountService accounts,
AuthService auth, AuthService auth,
GeoIpService geo, GeoService geo,
ActionLogService als, ActionLogService als,
RingService.RingServiceClient pusher, RingService.RingServiceClient pusher,
IConfiguration configuration, IConfiguration configuration,
@@ -30,12 +30,12 @@ public class AuthController(
public class ChallengeRequest public class ChallengeRequest
{ {
[Required] public ClientPlatform Platform { get; set; } [Required] public Shared.Models.ClientPlatform Platform { get; set; }
[Required] [MaxLength(256)] public string Account { get; set; } = null!; [Required] [MaxLength(256)] public string Account { get; set; } = null!;
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!; [Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
[MaxLength(1024)] public string? DeviceName { get; set; } [MaxLength(1024)] public string? DeviceName { get; set; }
public List<string> Audiences { get; set; } = new(); public List<string> Audiences { get; set; } = [];
public List<string> Scopes { get; set; } = new(); public List<string> Scopes { get; set; } = [];
} }
[HttpPost("challenge")] [HttpPost("challenge")]
@@ -61,9 +61,6 @@ public class AuthController(
request.DeviceName ??= userAgent; request.DeviceName ??= userAgent;
var device =
await auth.GetOrCreateDeviceAsync(account.Id, request.DeviceId, request.DeviceName, request.Platform);
// Trying to pick up challenges from the same IP address and user agent // Trying to pick up challenges from the same IP address and user agent
var existingChallenge = await db.AuthChallenges var existingChallenge = await db.AuthChallenges
.Where(e => e.AccountId == account.Id) .Where(e => e.AccountId == account.Id)
@@ -71,15 +68,9 @@ public class AuthController(
.Where(e => e.UserAgent == userAgent) .Where(e => e.UserAgent == userAgent)
.Where(e => e.StepRemain > 0) .Where(e => e.StepRemain > 0)
.Where(e => e.ExpiredAt != null && now < e.ExpiredAt) .Where(e => e.ExpiredAt != null && now < e.ExpiredAt)
.Where(e => e.Type == Shared.Models.ChallengeType.Login) .Where(e => e.DeviceId == request.DeviceId)
.Where(e => e.ClientId == device.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (existingChallenge is not null) if (existingChallenge is not null) return existingChallenge;
{
var existingSession = await db.AuthSessions.Where(e => e.ChallengeId == existingChallenge.Id)
.FirstOrDefaultAsync();
if (existingSession is null) return existingChallenge;
}
var challenge = new SnAuthChallenge var challenge = new SnAuthChallenge
{ {
@@ -90,7 +81,9 @@ public class AuthController(
IpAddress = ipAddress, IpAddress = ipAddress,
UserAgent = userAgent, UserAgent = userAgent,
Location = geo.GetPointFromIp(ipAddress), Location = geo.GetPointFromIp(ipAddress),
ClientId = device.Id, DeviceId = request.DeviceId,
DeviceName = request.DeviceName,
Platform = request.Platform,
AccountId = account.Id AccountId = account.Id
}.Normalize(); }.Normalize();
@@ -112,14 +105,11 @@ public class AuthController(
.ThenInclude(e => e.Profile) .ThenInclude(e => e.Profile)
.FirstOrDefaultAsync(e => e.Id == id); .FirstOrDefaultAsync(e => e.Id == id);
if (challenge is null) if (challenge is not null) return challenge;
{ logger.LogWarning("GetChallenge: challenge not found (challengeId={ChallengeId}, ip={IpAddress})",
logger.LogWarning("GetChallenge: challenge not found (challengeId={ChallengeId}, ip={IpAddress})", id, HttpContext.Connection.RemoteIpAddress?.ToString());
id, HttpContext.Connection.RemoteIpAddress?.ToString()); return NotFound("Auth challenge was not found.");
return NotFound("Auth challenge was not found.");
}
return challenge;
} }
[HttpGet("challenge/{id:guid}/factors")] [HttpGet("challenge/{id:guid}/factors")]
@@ -176,7 +166,6 @@ public class AuthController(
{ {
var challenge = await db.AuthChallenges var challenge = await db.AuthChallenges
.Include(e => e.Account) .Include(e => e.Account)
.Include(authChallenge => authChallenge.Client)
.FirstOrDefaultAsync(e => e.Id == id); .FirstOrDefaultAsync(e => e.Id == id);
if (challenge is null) return NotFound("Auth challenge was not found."); if (challenge is null) return NotFound("Auth challenge was not found.");
@@ -218,7 +207,7 @@ public class AuthController(
throw new ArgumentException("Invalid password."); throw new ArgumentException("Invalid password.");
} }
} }
catch (Exception ex) catch (Exception)
{ {
challenge.FailedAttempts++; challenge.FailedAttempts++;
db.Update(challenge); db.Update(challenge);
@@ -231,8 +220,11 @@ public class AuthController(
); );
await db.SaveChangesAsync(); await db.SaveChangesAsync();
logger.LogWarning("DoChallenge: authentication failure (challengeId={ChallengeId}, factorId={FactorId}, accountId={AccountId}, failedAttempts={FailedAttempts}, factorType={FactorType}, ip={IpAddress}, uaLength={UaLength})", logger.LogWarning(
challenge.Id, factor.Id, challenge.AccountId, challenge.FailedAttempts, factor.Type, HttpContext.Connection.RemoteIpAddress?.ToString(), (HttpContext.Request.Headers.UserAgent.ToString() ?? "").Length); "DoChallenge: authentication failure (challengeId={ChallengeId}, factorId={FactorId}, accountId={AccountId}, failedAttempts={FailedAttempts}, factorType={FactorType}, ip={IpAddress}, uaLength={UaLength})",
challenge.Id, factor.Id, challenge.AccountId, challenge.FailedAttempts, factor.Type,
HttpContext.Connection.RemoteIpAddress?.ToString(),
HttpContext.Request.Headers.UserAgent.ToString().Length);
return BadRequest("Invalid password."); return BadRequest("Invalid password.");
} }
@@ -242,11 +234,11 @@ public class AuthController(
AccountService.SetCultureInfo(challenge.Account); AccountService.SetCultureInfo(challenge.Account);
await pusher.SendPushNotificationToUserAsync(new SendPushNotificationToUserRequest await pusher.SendPushNotificationToUserAsync(new SendPushNotificationToUserRequest
{ {
Notification = new PushNotification() Notification = new PushNotification
{ {
Topic = "auth.login", Topic = "auth.login",
Title = localizer["NewLoginTitle"], Title = localizer["NewLoginTitle"],
Body = localizer["NewLoginBody", challenge.Client?.DeviceName ?? "unknown", Body = localizer["NewLoginBody", challenge.DeviceName ?? "unknown",
challenge.IpAddress ?? "unknown"], challenge.IpAddress ?? "unknown"],
IsSavable = true IsSavable = true
}, },
@@ -277,6 +269,14 @@ public class AuthController(
public string Token { get; set; } = string.Empty; public string Token { get; set; } = string.Empty;
} }
public class NewSessionRequest
{
[Required] [MaxLength(512)] public string DeviceId { get; set; } = null!;
[MaxLength(1024)] public string? DeviceName { get; set; }
[Required] public Shared.Models.ClientPlatform Platform { get; set; }
public Instant? ExpiredAt { get; set; }
}
[HttpPost("token")] [HttpPost("token")]
public async Task<ActionResult<TokenExchangeResponse>> ExchangeToken([FromBody] TokenExchangeRequest request) public async Task<ActionResult<TokenExchangeResponse>> ExchangeToken([FromBody] TokenExchangeRequest request)
{ {
@@ -327,4 +327,35 @@ public class AuthController(
}); });
return Ok(); return Ok();
} }
}
[HttpPost("login/session")]
[Microsoft.AspNetCore.Authorization.Authorize] // Use full namespace to avoid ambiguity with DysonNetwork.Pass.Permission.Authorize
public async Task<ActionResult<TokenExchangeResponse>> LoginFromSession([FromBody] NewSessionRequest request)
{
if (HttpContext.Items["CurrentUser"] is not SnAccount ||
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession)
return Unauthorized();
var newSession = await auth.CreateSessionFromParentAsync(
currentSession,
request.DeviceId,
request.DeviceName,
request.Platform,
request.ExpiredAt
);
var tk = auth.CreateToken(newSession);
// Set cookie using HttpContext, similar to CreateSessionAndIssueToken
HttpContext.Response.Cookies.Append(AuthConstants.CookieTokenName, tk, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Domain = _cookieDomain,
Expires = request.ExpiredAt?.ToDateTimeOffset() ?? DateTime.UtcNow.AddYears(20)
});
return Ok(new TokenExchangeResponse { Token = tk });
}
}

View File

@@ -2,6 +2,8 @@ using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
@@ -13,7 +15,8 @@ public class AuthService(
IConfiguration config, IConfiguration config,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
ICacheService cache ICacheService cache,
GeoService geo
) )
{ {
private HttpContext HttpContext => httpContextAccessor.HttpContext!; private HttpContext HttpContext => httpContextAccessor.HttpContext!;
@@ -30,7 +33,7 @@ public class AuthService(
{ {
// 1) Find out how many authentication factors the account has enabled. // 1) Find out how many authentication factors the account has enabled.
var enabledFactors = await db.AccountAuthFactors var enabledFactors = await db.AccountAuthFactors
.Where(f => f.AccountId == account.Id) .Where(f => f.AccountId == account.Id && f.Type != AccountAuthFactorType.PinCode)
.Where(f => f.EnabledAt != null) .Where(f => f.EnabledAt != null)
.ToListAsync(); .ToListAsync();
var maxSteps = enabledFactors.Count; var maxSteps = enabledFactors.Count;
@@ -41,13 +44,18 @@ public class AuthService(
// 2) Get login context from recent sessions // 2) Get login context from recent sessions
var recentSessions = await db.AuthSessions var recentSessions = await db.AuthSessions
.Include(s => s.Challenge)
.Where(s => s.AccountId == account.Id) .Where(s => s.AccountId == account.Id)
.Where(s => s.LastGrantedAt != null) .Where(s => s.LastGrantedAt != null)
.OrderByDescending(s => s.LastGrantedAt) .OrderByDescending(s => s.LastGrantedAt)
.Take(10) .Take(10)
.ToListAsync(); .ToListAsync();
var recentChallengeIds =
recentSessions
.Where(s => s.ChallengeId != null)
.Select(s => s.ChallengeId!.Value).ToList();
var recentChallenges = await db.AuthChallenges.Where(c => recentChallengeIds.Contains(c.Id)).ToListAsync();
var ipAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(); var ipAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = request.Headers.UserAgent.ToString(); var userAgent = request.Headers.UserAgent.ToString();
@@ -59,14 +67,14 @@ public class AuthService(
else else
{ {
// Check if IP has been used before // Check if IP has been used before
var ipPreviouslyUsed = recentSessions.Any(s => s.Challenge?.IpAddress == ipAddress); var ipPreviouslyUsed = recentChallenges.Any(c => c.IpAddress == ipAddress);
if (!ipPreviouslyUsed) if (!ipPreviouslyUsed)
{ {
riskScore += 8; riskScore += 8;
} }
// Check geographical distance for last known location // Check geographical distance for last known location
var lastKnownIp = recentSessions.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s.Challenge?.IpAddress))?.Challenge?.IpAddress; var lastKnownIp = recentChallenges.FirstOrDefault(c => !string.IsNullOrWhiteSpace(c.IpAddress))?.IpAddress;
if (!string.IsNullOrWhiteSpace(lastKnownIp) && lastKnownIp != ipAddress) if (!string.IsNullOrWhiteSpace(lastKnownIp) && lastKnownIp != ipAddress)
{ {
riskScore += 6; riskScore += 6;
@@ -80,9 +88,9 @@ public class AuthService(
} }
else else
{ {
var uaPreviouslyUsed = recentSessions.Any(s => var uaPreviouslyUsed = recentChallenges.Any(c =>
!string.IsNullOrWhiteSpace(s.Challenge?.UserAgent) && !string.IsNullOrWhiteSpace(c.UserAgent) &&
string.Equals(s.Challenge.UserAgent, userAgent, StringComparison.OrdinalIgnoreCase)); string.Equals(c.UserAgent, userAgent, StringComparison.OrdinalIgnoreCase));
if (!uaPreviouslyUsed) if (!uaPreviouslyUsed)
{ {
@@ -156,7 +164,7 @@ public class AuthService(
// 8) Device Trust Assessment // 8) Device Trust Assessment
var trustedDeviceIds = recentSessions var trustedDeviceIds = recentSessions
.Where(s => s.CreatedAt > now.Minus(Duration.FromDays(30))) // Trust devices from last 30 days .Where(s => s.CreatedAt > now.Minus(Duration.FromDays(30))) // Trust devices from last 30 days
.Select(s => s.Challenge?.ClientId) .Select(s => s.ClientId)
.Where(id => id.HasValue) .Where(id => id.HasValue)
.Distinct() .Distinct()
.ToList(); .ToList();
@@ -180,29 +188,28 @@ public class AuthService(
return totalRequiredSteps; return totalRequiredSteps;
} }
public async Task<SnAuthSession> CreateSessionForOidcAsync(SnAccount account, Instant time, public async Task<SnAuthSession> CreateSessionForOidcAsync(
Guid? customAppId = null) SnAccount account,
Instant time,
Guid? customAppId = null,
SnAuthSession? parentSession = null
)
{ {
var challenge = new SnAuthChallenge var ipAddr = HttpContext.Connection.RemoteIpAddress?.ToString();
{ var geoLocation = ipAddr is not null ? geo.GetPointFromIp(ipAddr) : null;
AccountId = account.Id,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = HttpContext.Request.Headers.UserAgent,
StepRemain = 1,
StepTotal = 1,
Type = customAppId is not null ? ChallengeType.OAuth : ChallengeType.Oidc
};
var session = new SnAuthSession var session = new SnAuthSession
{ {
AccountId = account.Id, AccountId = account.Id,
CreatedAt = time, CreatedAt = time,
LastGrantedAt = time, LastGrantedAt = time,
Challenge = challenge, IpAddress = ipAddr,
AppId = customAppId UserAgent = HttpContext.Request.Headers.UserAgent,
Location = geoLocation,
AppId = customAppId,
ParentSessionId = parentSession?.Id,
Type = customAppId is not null ? SessionType.OAuth : SessionType.Oidc,
}; };
db.AuthChallenges.Add(challenge);
db.AuthSessions.Add(session); db.AuthSessions.Add(session);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@@ -216,7 +223,8 @@ public class AuthService(
ClientPlatform platform = ClientPlatform.Unidentified ClientPlatform platform = ClientPlatform.Unidentified
) )
{ {
var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId); var device = await db.AuthClients
.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
if (device is not null) return device; if (device is not null) return device;
device = new SnAuthClient device = new SnAuthClient
{ {
@@ -287,35 +295,71 @@ public class AuthService(
/// <summary> /// <summary>
/// Immediately revoke a session by setting expiry to now and clearing from cache /// Immediately revoke a session by setting expiry to now and clearing from cache
/// This provides immediate invalidation of tokens and sessions /// This provides immediate invalidation of tokens and sessions, including all child sessions recursively.
/// </summary> /// </summary>
/// <param name="sessionId">Session ID to revoke</param> /// <param name="sessionId">Session ID to revoke</param>
/// <returns>True if session was found and revoked, false otherwise</returns> /// <returns>True if session was found and revoked, false otherwise</returns>
public async Task<bool> RevokeSessionAsync(Guid sessionId) public async Task<bool> RevokeSessionAsync(Guid sessionId)
{ {
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId); var sessionsToRevokeIds = new HashSet<Guid>();
if (session == null) await CollectSessionsToRevoke(sessionId, sessionsToRevokeIds);
if (sessionsToRevokeIds.Count == 0)
{ {
return false; return false;
} }
// Set expiry to now (immediate invalidation)
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
session.ExpiredAt = now; var accountIdsToClearCache = new HashSet<Guid>();
db.AuthSessions.Update(session);
// Clear from cache immediately // Fetch all sessions to be revoked in one go
var cacheKey = $"{AuthCachePrefix}{session.Id}"; var sessions = await db.AuthSessions
await cache.RemoveAsync(cacheKey); .Where(s => sessionsToRevokeIds.Contains(s.Id))
.ToListAsync();
// Clear account-level cache groups that include this session foreach (var session in sessions)
await cache.RemoveAsync($"{AuthCachePrefix}{session.AccountId}"); {
session.ExpiredAt = now;
accountIdsToClearCache.Add(session.AccountId);
// Clear from cache immediately for each session
await cache.RemoveAsync($"{AuthCachePrefix}{session.Id}");
}
db.AuthSessions.UpdateRange(sessions);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Clear account-level cache groups
foreach (var accountId in accountIdsToClearCache)
{
await cache.RemoveAsync($"{AuthCachePrefix}{accountId}");
}
return true; return true;
} }
/// <summary>
/// Recursively collects all session IDs that need to be revoked, starting from a given session.
/// </summary>
/// <param name="currentSessionId">The session ID to start collecting from.</param>
/// <param name="sessionsToRevoke">A HashSet to store the IDs of all sessions to be revoked.</param>
private async Task CollectSessionsToRevoke(Guid currentSessionId, HashSet<Guid> sessionsToRevoke)
{
if (!sessionsToRevoke.Add(currentSessionId))
return; // Already processed this session
// Find direct children
var childSessions = await db.AuthSessions
.Where(s => s.ParentSessionId == currentSessionId)
.Select(s => s.Id)
.ToListAsync();
foreach (var childId in childSessions)
{
await CollectSessionsToRevoke(childId, sessionsToRevoke);
}
}
/// <summary> /// <summary>
/// Revoke all sessions for an account (logout everywhere) /// Revoke all sessions for an account (logout everywhere)
/// </summary> /// </summary>
@@ -374,10 +418,12 @@ public class AuthService(
if (challenge.StepRemain != 0) if (challenge.StepRemain != 0)
throw new ArgumentException("Challenge not yet completed."); throw new ArgumentException("Challenge not yet completed.");
var hasSession = await db.AuthSessions var device = await GetOrCreateDeviceAsync(
.AnyAsync(e => e.ChallengeId == challenge.Id); challenge.AccountId,
if (hasSession) challenge.DeviceId,
throw new ArgumentException("Session already exists for this challenge."); challenge.DeviceName,
challenge.Platform
);
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var session = new SnAuthSession var session = new SnAuthSession
@@ -385,7 +431,13 @@ public class AuthService(
LastGrantedAt = now, LastGrantedAt = now,
ExpiredAt = now.Plus(Duration.FromDays(7)), ExpiredAt = now.Plus(Duration.FromDays(7)),
AccountId = challenge.AccountId, AccountId = challenge.AccountId,
ChallengeId = challenge.Id IpAddress = challenge.IpAddress,
UserAgent = challenge.UserAgent,
Location = challenge.Location,
Scopes = challenge.Scopes,
Audiences = challenge.Audiences,
ChallengeId = challenge.Id,
ClientId = device.Id,
}; };
db.AuthSessions.Add(session); db.AuthSessions.Add(session);
@@ -408,7 +460,7 @@ public class AuthService(
return tk; return tk;
} }
private string CreateCompactToken(Guid sessionId, RSA rsa) private static string CreateCompactToken(Guid sessionId, RSA rsa)
{ {
// Create the payload: just the session ID // Create the payload: just the session ID
var payloadBytes = sessionId.ToByteArray(); var payloadBytes = sessionId.ToByteArray();
@@ -499,7 +551,8 @@ public class AuthService(
return key; return key;
} }
public async Task<SnApiKey> CreateApiKey(Guid accountId, string label, Instant? expiredAt = null) public async Task<SnApiKey> CreateApiKey(Guid accountId, string label, Instant? expiredAt = null,
SnAuthSession? parentSession = null)
{ {
var key = new SnApiKey var key = new SnApiKey
{ {
@@ -508,7 +561,8 @@ public class AuthService(
Session = new SnAuthSession Session = new SnAuthSession
{ {
AccountId = accountId, AccountId = accountId,
ExpiredAt = expiredAt ExpiredAt = expiredAt,
ParentSessionId = parentSession?.Id
}, },
}; };
@@ -614,4 +668,47 @@ public class AuthService(
return Convert.FromBase64String(padded); return Convert.FromBase64String(padded);
} }
}
/// <summary>
/// Creates a new session derived from an existing parent session.
/// </summary>
/// <param name="parentSession">The existing session from which the new session is derived.</param>
/// <param name="deviceId">The ID of the device for the new session.</param>
/// <param name="deviceName">The name of the device for the new session.</param>
/// <param name="platform">The platform of the device for the new session.</param>
/// <param name="expiredAt">Optional: The expiration time for the new session.</param>
/// <returns>The newly created SnAuthSession.</returns>
public async Task<SnAuthSession> CreateSessionFromParentAsync(
SnAuthSession parentSession,
string deviceId,
string? deviceName,
ClientPlatform platform,
Instant? expiredAt = null
)
{
var device = await GetOrCreateDeviceAsync(parentSession.AccountId, deviceId, deviceName, platform);
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
var geoLocation = ipAddress is not null ? geo.GetPointFromIp(ipAddress) : null;
var now = SystemClock.Instance.GetCurrentInstant();
var session = new SnAuthSession
{
IpAddress = ipAddress,
UserAgent = userAgent,
Location = geoLocation,
AccountId = parentSession.AccountId,
CreatedAt = now,
LastGrantedAt = now,
ExpiredAt = expiredAt,
ParentSessionId = parentSession.Id,
ClientId = device.Id,
};
db.AuthSessions.Add(session);
await db.SaveChangesAsync();
return session;
}
}

View File

@@ -306,7 +306,7 @@ public class OidcProviderController(
HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized(); HttpContext.Items["CurrentSession"] is not SnAuthSession currentSession) return Unauthorized();
// Get requested scopes from the token // Get requested scopes from the token
var scopes = currentSession.Challenge?.Scopes ?? []; var scopes = currentSession.Scopes;
var userInfo = new Dictionary<string, object> var userInfo = new Dictionary<string, object>
{ {

View File

@@ -72,7 +72,6 @@ public class OidcProviderService(
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var queryable = db.AuthSessions var queryable = db.AuthSessions
.Include(s => s.Challenge)
.AsQueryable(); .AsQueryable();
if (withAccount) if (withAccount)
queryable = queryable queryable = queryable
@@ -85,8 +84,7 @@ public class OidcProviderService(
.Where(s => s.AccountId == accountId && .Where(s => s.AccountId == accountId &&
s.AppId == clientId && s.AppId == clientId &&
(s.ExpiredAt == null || s.ExpiredAt > now) && (s.ExpiredAt == null || s.ExpiredAt > now) &&
s.Challenge != null && s.Type == Shared.Models.SessionType.OAuth)
s.Challenge.Type == Shared.Models.ChallengeType.OAuth)
.OrderByDescending(s => s.CreatedAt) .OrderByDescending(s => s.CreatedAt)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
@@ -511,7 +509,6 @@ public class OidcProviderService(
{ {
return await db.AuthSessions return await db.AuthSessions
.Include(s => s.Account) .Include(s => s.Account)
.Include(s => s.Challenge)
.FirstOrDefaultAsync(s => s.Id == sessionId); .FirstOrDefaultAsync(s => s.Id == sessionId);
} }

View File

@@ -342,13 +342,19 @@ public class ConnectionController(
callbackData.State.Split('|').FirstOrDefault() : callbackData.State.Split('|').FirstOrDefault() :
string.Empty; string.Empty;
var challenge = await oidcService.CreateChallengeForUserAsync( if (HttpContext.Items["CurrentSession"] is not SnAuthSession parentSession) parentSession = null;
var session = await oidcService.CreateSessionForUserAsync(
userInfo, userInfo,
connection.Account, connection.Account,
HttpContext, HttpContext,
deviceId ?? string.Empty); deviceId ?? string.Empty,
null,
ClientPlatform.Web,
parentSession);
var redirectUrl = QueryHelpers.AddQueryString(redirectBaseUrl, "challenge", challenge.Id.ToString()); var token = auth.CreateToken(session);
var redirectUrl = QueryHelpers.AddQueryString(redirectBaseUrl, "token", token);
logger.LogInformation("OIDC login successful for user {UserId}. Redirecting to {RedirectUrl}", connection.AccountId, redirectUrl); logger.LogInformation("OIDC login successful for user {UserId}. Redirecting to {RedirectUrl}", connection.AccountId, redirectUrl);
return Redirect(redirectUrl); return Redirect(redirectUrl);
} }

View File

@@ -14,6 +14,7 @@ public class OidcController(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
AppDatabase db, AppDatabase db,
AccountService accounts, AccountService accounts,
AuthService auth,
ICacheService cache, ICacheService cache,
ILogger<OidcController> logger ILogger<OidcController> logger
) )
@@ -75,7 +76,7 @@ public class OidcController(
/// Handles Apple authentication directly from mobile apps /// Handles Apple authentication directly from mobile apps
/// </summary> /// </summary>
[HttpPost("apple/mobile")] [HttpPost("apple/mobile")]
public async Task<ActionResult<SnAuthChallenge>> AppleMobileLogin( public async Task<ActionResult<AuthController.TokenExchangeResponse>> AppleMobileLogin(
[FromBody] AppleMobileSignInRequest request [FromBody] AppleMobileSignInRequest request
) )
{ {
@@ -98,16 +99,21 @@ public class OidcController(
// Find or create user account using existing logic // Find or create user account using existing logic
var account = await FindOrCreateAccount(userInfo, "apple"); var account = await FindOrCreateAccount(userInfo, "apple");
if (HttpContext.Items["CurrentSession"] is not SnAuthSession parentSession) parentSession = null;
// Create session using the OIDC service // Create session using the OIDC service
var challenge = await appleService.CreateChallengeForUserAsync( var session = await appleService.CreateSessionForUserAsync(
userInfo, userInfo,
account, account,
HttpContext, HttpContext,
request.DeviceId, request.DeviceId,
request.DeviceName request.DeviceName,
ClientPlatform.Ios,
parentSession
); );
return Ok(challenge); var token = auth.CreateToken(session);
return Ok(new AuthController.TokenExchangeResponse { Token = token });
} }
catch (SecurityTokenValidationException ex) catch (SecurityTokenValidationException ex)
{ {

View File

@@ -1,4 +1,3 @@
using System;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@@ -250,15 +249,17 @@ public abstract class OidcService(
} }
/// <summary> /// <summary>
/// Creates a challenge and session for an authenticated user /// Creates a session for an authenticated user
/// Also creates or updates the account connection /// Also creates or updates the account connection
/// </summary> /// </summary>
public async Task<SnAuthChallenge> CreateChallengeForUserAsync( public async Task<SnAuthSession> CreateSessionForUserAsync(
OidcUserInfo userInfo, OidcUserInfo userInfo,
SnAccount account, SnAccount account,
HttpContext request, HttpContext request,
string deviceId, string deviceId,
string? deviceName = null string? deviceName = null,
ClientPlatform platform = ClientPlatform.Web,
SnAuthSession? parentSession = null
) )
{ {
// Create or update the account connection // Create or update the account connection
@@ -282,28 +283,24 @@ public abstract class OidcService(
await Db.AccountConnections.AddAsync(connection); await Db.AccountConnections.AddAsync(connection);
} }
// Create a challenge that's already completed // Create a session directly
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var device = await auth.GetOrCreateDeviceAsync(account.Id, deviceId, deviceName, ClientPlatform.Ios); var device = await auth.GetOrCreateDeviceAsync(account.Id, deviceId, deviceName, platform);
var challenge = new SnAuthChallenge
{
ExpiredAt = now.Plus(Duration.FromHours(1)),
StepTotal = await auth.DetectChallengeRisk(request.Request, account),
Type = ChallengeType.Oidc,
Audiences = [ProviderName],
Scopes = ["*"],
AccountId = account.Id,
ClientId = device.Id,
IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null,
UserAgent = request.Request.Headers.UserAgent,
};
challenge.StepRemain--;
if (challenge.StepRemain < 0) challenge.StepRemain = 0;
await Db.AuthChallenges.AddAsync(challenge); var session = new SnAuthSession
{
AccountId = account.Id,
CreatedAt = now,
LastGrantedAt = now,
ParentSessionId = parentSession?.Id,
ClientId = device.Id,
ExpiredAt = now.Plus(Duration.FromDays(30))
};
await Db.AuthSessions.AddAsync(session);
await Db.SaveChangesAsync(); await Db.SaveChangesAsync();
return challenge; return session;
} }
} }

View File

@@ -77,7 +77,7 @@ public class TokenAuthService(
"AuthenticateTokenAsync: success via cache (sessionId={SessionId}, accountId={AccountId}, scopes={ScopeCount}, expiresAt={ExpiresAt})", "AuthenticateTokenAsync: success via cache (sessionId={SessionId}, accountId={AccountId}, scopes={ScopeCount}, expiresAt={ExpiresAt})",
sessionId, sessionId,
session.AccountId, session.AccountId,
session.Challenge?.Scopes.Count, session.Scopes.Count,
session.ExpiredAt session.ExpiredAt
); );
return (true, session, null); return (true, session, null);
@@ -87,8 +87,7 @@ public class TokenAuthService(
session = await db.AuthSessions session = await db.AuthSessions
.AsNoTracking() .AsNoTracking()
.Include(e => e.Challenge) .Include(e => e.Client)
.ThenInclude(e => e.Client)
.Include(e => e.Account) .Include(e => e.Account)
.ThenInclude(e => e.Profile) .ThenInclude(e => e.Profile)
.FirstOrDefaultAsync(s => s.Id == sessionId); .FirstOrDefaultAsync(s => s.Id == sessionId);
@@ -110,11 +109,11 @@ public class TokenAuthService(
"AuthenticateTokenAsync: DB session loaded (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId}, appId={AppId}, scopes={ScopeCount}, ip={Ip}, uaLen={UaLen})", "AuthenticateTokenAsync: DB session loaded (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId}, appId={AppId}, scopes={ScopeCount}, ip={Ip}, uaLen={UaLen})",
sessionId, sessionId,
session.AccountId, session.AccountId,
session.Challenge?.ClientId, session.ClientId,
session.AppId, session.AppId,
session.Challenge?.Scopes.Count, session.Scopes.Count,
session.Challenge?.IpAddress, session.IpAddress,
(session.Challenge?.UserAgent ?? string.Empty).Length (session.UserAgent ?? string.Empty).Length
); );
logger.LogDebug("AuthenticateTokenAsync: enriching account with subscription (accountId={AccountId})", session.AccountId); logger.LogDebug("AuthenticateTokenAsync: enriching account with subscription (accountId={AccountId})", session.AccountId);
@@ -143,7 +142,7 @@ public class TokenAuthService(
"AuthenticateTokenAsync: success via DB (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId})", "AuthenticateTokenAsync: success via DB (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId})",
sessionId, sessionId,
session.AccountId, session.AccountId,
session.Challenge?.ClientId session.ClientId
); );
return (true, session, null); return (true, session, null);
} }

View File

@@ -132,4 +132,8 @@
<AdditionalFiles Include="Resources\Emails\PasswordResetEmail.razor" /> <AdditionalFiles Include="Resources\Emails\PasswordResetEmail.razor" />
<AdditionalFiles Include="Resources\Emails\RegistrationConfirmEmail.razor" /> <AdditionalFiles Include="Resources\Emails\RegistrationConfirmEmail.razor" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project> </Project>

View File

@@ -6,16 +6,17 @@ using Quartz;
namespace DysonNetwork.Pass.Handlers; namespace DysonNetwork.Pass.Handlers;
public class ActionLogFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<SnActionLog> public class ActionLogFlushHandler(IServiceProvider sp) : IFlushHandler<SnActionLog>
{ {
public async Task FlushAsync(IReadOnlyList<SnActionLog> items) public async Task FlushAsync(IReadOnlyList<SnActionLog> items)
{ {
using var scope = serviceProvider.CreateScope(); using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
var now = SystemClock.Instance.GetCurrentInstant();
await db.BulkInsertAsync(items.Select(x => await db.BulkInsertAsync(items.Select(x =>
{ {
x.CreatedAt = SystemClock.Instance.GetCurrentInstant(); x.CreatedAt = now;
x.UpdatedAt = x.CreatedAt; x.UpdatedAt = x.CreatedAt;
return x; return x;
}), config => config.ConflictOption = ConflictOption.Ignore); }), config => config.ConflictOption = ConflictOption.Ignore);

View File

@@ -24,7 +24,7 @@ public class ExperienceService(AppDatabase db, SubscriptionService subscriptions
{ {
SubscriptionType.Stellar => 1.5, SubscriptionType.Stellar => 1.5,
SubscriptionType.Nova => 2, SubscriptionType.Nova => 2,
SubscriptionType.Supernova => 2, SubscriptionType.Supernova => 2.5,
_ => 1 _ => 1
}; };
if (record.Delta >= 0) if (record.Delta >= 0)

View File

@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Pass.Wallet; using DysonNetwork.Pass.Wallet;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Auth;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -81,7 +82,7 @@ public class LotteryController(AppDatabase db, LotteryService lotteryService) :
[HttpPost("draw")] [HttpPost("draw")]
[Authorize] [Authorize]
[RequiredPermission("maintenance", "lotteries.draw.perform")] [AskPermission("lotteries.draw.perform")]
public async Task<IActionResult> PerformLotteryDraw() public async Task<IActionResult> PerformLotteryDraw()
{ {
await lotteryService.DrawLotteries(); await lotteryService.DrawLotteries();

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class ReinitalMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "background_id",
table: "account_profiles");
migrationBuilder.DropColumn(
name: "picture_id",
table: "account_profiles");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "background_id",
table: "account_profiles",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "picture_id",
table: "account_profiles",
type: "character varying(32)",
maxLength: 32,
nullable: true);
}
}
}

View File

@@ -1,94 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RemoveNotification : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "notification_push_subscriptions");
migrationBuilder.DropTable(
name: "notifications");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "notification_push_subscriptions",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
device_id = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
device_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
provider = table.Column<int>(type: "integer", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_notification_push_subscriptions", x => x.id);
table.ForeignKey(
name: "fk_notification_push_subscriptions_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "notifications",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
priority = table.Column<int>(type: "integer", nullable: false),
subtitle = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
topic = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
viewed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_notifications", x => x.id);
table.ForeignKey(
name: "fk_notifications_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_notification_push_subscriptions_account_id",
table: "notification_push_subscriptions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_notification_push_subscriptions_device_token_device_id_acco",
table: "notification_push_subscriptions",
columns: new[] { "device_token", "device_id", "account_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_notifications_account_id",
table: "notifications",
column: "account_id");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddCheckInBackdated : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Instant>(
name: "backdated_from",
table: "account_check_in_results",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backdated_from",
table: "account_check_in_results");
}
}
}

View File

@@ -1,130 +0,0 @@
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RemoveDevelopers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_sessions_custom_apps_app_id",
table: "auth_sessions");
migrationBuilder.DropTable(
name: "custom_app_secrets");
migrationBuilder.DropTable(
name: "custom_apps");
migrationBuilder.DropIndex(
name: "ix_auth_sessions_app_id",
table: "auth_sessions");
migrationBuilder.CreateTable(
name: "punishments",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
reason = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
type = table.Column<int>(type: "integer", nullable: false),
blocked_permissions = table.Column<List<string>>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_punishments", x => x.id);
table.ForeignKey(
name: "fk_punishments_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_punishments_account_id",
table: "punishments",
column: "account_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "punishments");
migrationBuilder.CreateTable(
name: "custom_apps",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_custom_apps", x => x.id);
});
migrationBuilder.CreateTable(
name: "custom_app_secrets",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
app_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_oidc = table.Column<bool>(type: "boolean", nullable: false),
secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_custom_app_secrets", x => x.id);
table.ForeignKey(
name: "fk_custom_app_secrets_custom_apps_app_id",
column: x => x.app_id,
principalTable: "custom_apps",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_auth_sessions_app_id",
table: "auth_sessions",
column: "app_id");
migrationBuilder.CreateIndex(
name: "ix_custom_app_secrets_app_id",
table: "custom_app_secrets",
column: "app_id");
migrationBuilder.AddForeignKey(
name: "fk_auth_sessions_custom_apps_app_id",
table: "auth_sessions",
column: "app_id",
principalTable: "custom_apps",
principalColumn: "id");
}
}
}

View File

@@ -1,28 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddProfileLinks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "links",
table: "account_profiles",
type: "jsonb",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "links",
table: "account_profiles");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddPublicContact : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "is_public",
table: "account_contacts",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "is_public",
table: "account_contacts");
}
}
}

View File

@@ -1,109 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddAuthorizeDevice : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "device_id",
table: "auth_challenges",
type: "character varying(1024)",
maxLength: 1024,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AddColumn<Guid>(
name: "client_id",
table: "auth_challenges",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "auth_clients",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
device_name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
device_label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
device_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_auth_clients", x => x.id);
table.ForeignKey(
name: "fk_auth_clients_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_auth_challenges_client_id",
table: "auth_challenges",
column: "client_id");
migrationBuilder.CreateIndex(
name: "ix_auth_clients_account_id",
table: "auth_clients",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_auth_clients_device_id",
table: "auth_clients",
column: "device_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "fk_auth_challenges_auth_clients_client_id",
table: "auth_challenges",
column: "client_id",
principalTable: "auth_clients",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_challenges_auth_clients_client_id",
table: "auth_challenges");
migrationBuilder.DropTable(
name: "auth_clients");
migrationBuilder.DropIndex(
name: "ix_auth_challenges_client_id",
table: "auth_challenges");
migrationBuilder.DropColumn(
name: "client_id",
table: "auth_challenges");
migrationBuilder.AlterColumn<string>(
name: "device_id",
table: "auth_challenges",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(1024)",
oldMaxLength: 1024,
oldNullable: true);
}
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddAuthDevicePlatform : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "platform",
table: "auth_challenges");
migrationBuilder.AddColumn<int>(
name: "platform",
table: "auth_clients",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "platform",
table: "auth_clients");
migrationBuilder.AddColumn<int>(
name: "platform",
table: "auth_challenges",
type: "integer",
nullable: false,
defaultValue: 0);
}
}
}

View File

@@ -1,28 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RemoveAuthClientIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "ix_auth_clients_device_id",
table: "auth_clients");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "ix_auth_clients_device_id",
table: "auth_clients",
column: "device_id",
unique: true);
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RemoveChallengeOldDeviceId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "device_id",
table: "auth_challenges");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "device_id",
table: "auth_challenges",
type: "character varying(1024)",
maxLength: 1024,
nullable: true);
}
}
}

View File

@@ -1,113 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddApiKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_sessions_auth_challenges_challenge_id",
table: "auth_sessions");
migrationBuilder.DropColumn(
name: "label",
table: "auth_sessions");
migrationBuilder.AlterColumn<Guid>(
name: "challenge_id",
table: "auth_sessions",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.CreateTable(
name: "api_keys",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
session_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_api_keys", x => x.id);
table.ForeignKey(
name: "fk_api_keys_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_api_keys_auth_sessions_session_id",
column: x => x.session_id,
principalTable: "auth_sessions",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_api_keys_account_id",
table: "api_keys",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_api_keys_session_id",
table: "api_keys",
column: "session_id");
migrationBuilder.AddForeignKey(
name: "fk_auth_sessions_auth_challenges_challenge_id",
table: "auth_sessions",
column: "challenge_id",
principalTable: "auth_challenges",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_auth_sessions_auth_challenges_challenge_id",
table: "auth_sessions");
migrationBuilder.DropTable(
name: "api_keys");
migrationBuilder.AlterColumn<Guid>(
name: "challenge_id",
table: "auth_sessions",
type: "uuid",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AddColumn<string>(
name: "label",
table: "auth_sessions",
type: "character varying(1024)",
maxLength: 1024,
nullable: true);
migrationBuilder.AddForeignKey(
name: "fk_auth_sessions_auth_challenges_challenge_id",
table: "auth_sessions",
column: "challenge_id",
principalTable: "auth_challenges",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddLevelingBonusMultiplier : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "bonus_multiplier",
table: "experience_records",
type: "double precision",
nullable: false,
defaultValue: 0.0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "bonus_multiplier",
table: "experience_records");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class CacheSocialCreditsInProfile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "social_credits",
table: "account_profiles",
type: "double precision",
nullable: false,
defaultValue: 0.0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "social_credits",
table: "account_profiles");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddOrderProductIdentifier : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "product_identifier",
table: "payment_orders",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "product_identifier",
table: "payment_orders");
}
}
}

View File

@@ -1,30 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddAccountRegion : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "region",
table: "accounts",
type: "character varying(32)",
maxLength: 32,
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "region",
table: "accounts");
}
}
}

View File

@@ -1,63 +0,0 @@
using DysonNetwork.Shared.GeoIp;
using Microsoft.EntityFrameworkCore.Migrations;
using NetTopologySuite.Geometries;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RefactorGeoIpPoint : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE auth_challenges SET location = NULL;");
migrationBuilder.Sql("UPDATE action_logs SET location = NULL;");
migrationBuilder.DropColumn(
name: "location",
table: "auth_challenges");
migrationBuilder.AddColumn<GeoPoint>(
name: "location",
table: "auth_challenges",
type: "jsonb",
nullable: true);
migrationBuilder.DropColumn(
name: "location",
table: "action_logs");
migrationBuilder.AddColumn<GeoPoint>(
name: "location",
table: "action_logs",
type: "jsonb",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "location",
table: "auth_challenges");
migrationBuilder.AddColumn<Point>(
name: "location",
table: "auth_challenges",
type: "geometry",
nullable: true);
migrationBuilder.DropColumn(
name: "location",
table: "action_logs");
migrationBuilder.AddColumn<Point>(
name: "location",
table: "action_logs",
type: "geometry",
nullable: true);
}
}
}

View File

@@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RemoveNetTopo : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.OldAnnotation("Npgsql:PostgresExtension:postgis", ",,");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
}
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddAutomatedStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "app_identifier",
table: "account_statuses",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "is_automated",
table: "account_statuses",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "app_identifier",
table: "account_statuses");
migrationBuilder.DropColumn(
name: "is_automated",
table: "account_statuses");
}
}
}

View File

@@ -1,28 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddStatusMeta : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, object>>(
name: "meta",
table: "account_statuses",
type: "jsonb",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "meta",
table: "account_statuses");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddSubscriptionGift : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "wallet_gifts",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
gifter_id = table.Column<Guid>(type: "uuid", nullable: false),
recipient_id = table.Column<Guid>(type: "uuid", nullable: true),
gift_code = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
message = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
subscription_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
base_price = table.Column<decimal>(type: "numeric", nullable: false),
final_price = table.Column<decimal>(type: "numeric", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
redeemed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
redeemer_id = table.Column<Guid>(type: "uuid", nullable: true),
expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
is_open_gift = table.Column<bool>(type: "boolean", nullable: false),
payment_method = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
payment_details = table.Column<SnPaymentDetails>(type: "jsonb", nullable: false),
coupon_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_gifts", x => x.id);
table.ForeignKey(
name: "fk_wallet_gifts_accounts_gifter_id",
column: x => x.gifter_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_wallet_gifts_accounts_recipient_id",
column: x => x.recipient_id,
principalTable: "accounts",
principalColumn: "id");
table.ForeignKey(
name: "fk_wallet_gifts_accounts_redeemer_id",
column: x => x.redeemer_id,
principalTable: "accounts",
principalColumn: "id");
table.ForeignKey(
name: "fk_wallet_gifts_wallet_coupons_coupon_id",
column: x => x.coupon_id,
principalTable: "wallet_coupons",
principalColumn: "id");
});
migrationBuilder.AddColumn<Guid>(
name: "gift_id",
table: "wallet_subscriptions",
type: "uuid",
nullable: true);
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_coupon_id",
table: "wallet_gifts",
column: "coupon_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_gift_code",
table: "wallet_gifts",
column: "gift_code");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_gifter_id",
table: "wallet_gifts",
column: "gifter_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_recipient_id",
table: "wallet_gifts",
column: "recipient_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_redeemer_id",
table: "wallet_gifts",
column: "redeemer_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_gift_id",
table: "wallet_subscriptions",
column: "gift_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "fk_wallet_subscriptions_wallet_gifts_gift_id",
table: "wallet_subscriptions",
column: "gift_id",
principalTable: "wallet_gifts",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_wallet_subscriptions_wallet_gifts_gift_id",
table: "wallet_subscriptions");
migrationBuilder.DropTable(
name: "wallet_gifts");
migrationBuilder.DropIndex(
name: "ix_wallet_subscriptions_gift_id",
table: "wallet_subscriptions");
migrationBuilder.DropColumn(
name: "gift_id",
table: "wallet_subscriptions");
}
}
}

View File

@@ -1,81 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RefactorSubscriptionRelation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_wallet_subscriptions_wallet_gifts_gift_id",
table: "wallet_subscriptions");
migrationBuilder.DropIndex(
name: "ix_wallet_subscriptions_gift_id",
table: "wallet_subscriptions");
migrationBuilder.DropColumn(
name: "gift_id",
table: "wallet_subscriptions");
migrationBuilder.AddColumn<Guid>(
name: "subscription_id",
table: "wallet_gifts",
type: "uuid",
nullable: true);
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_subscription_id",
table: "wallet_gifts",
column: "subscription_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "fk_wallet_gifts_wallet_subscriptions_subscription_id",
table: "wallet_gifts",
column: "subscription_id",
principalTable: "wallet_subscriptions",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_wallet_gifts_wallet_subscriptions_subscription_id",
table: "wallet_gifts");
migrationBuilder.DropIndex(
name: "ix_wallet_gifts_subscription_id",
table: "wallet_gifts");
migrationBuilder.DropColumn(
name: "subscription_id",
table: "wallet_gifts");
migrationBuilder.AddColumn<Guid>(
name: "gift_id",
table: "wallet_subscriptions",
type: "uuid",
nullable: true);
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_gift_id",
table: "wallet_subscriptions",
column: "gift_id",
unique: true);
migrationBuilder.AddForeignKey(
name: "fk_wallet_subscriptions_wallet_gifts_gift_id",
table: "wallet_subscriptions",
column: "gift_id",
principalTable: "wallet_gifts",
principalColumn: "id");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,99 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddWalletFund : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "wallet_funds",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
total_amount = table.Column<decimal>(type: "numeric", nullable: false),
split_type = table.Column<int>(type: "integer", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
message = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
creator_account_id = table.Column<Guid>(type: "uuid", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_funds", x => x.id);
table.ForeignKey(
name: "fk_wallet_funds_accounts_creator_account_id",
column: x => x.creator_account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "wallet_fund_recipients",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
fund_id = table.Column<Guid>(type: "uuid", nullable: false),
recipient_account_id = table.Column<Guid>(type: "uuid", nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false),
is_received = table.Column<bool>(type: "boolean", nullable: false),
received_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_fund_recipients", x => x.id);
table.ForeignKey(
name: "fk_wallet_fund_recipients_accounts_recipient_account_id",
column: x => x.recipient_account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_wallet_fund_recipients_wallet_funds_fund_id",
column: x => x.fund_id,
principalTable: "wallet_funds",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_wallet_fund_recipients_fund_id",
table: "wallet_fund_recipients",
column: "fund_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_fund_recipients_recipient_account_id",
table: "wallet_fund_recipients",
column: "recipient_account_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_funds_creator_account_id",
table: "wallet_funds",
column: "creator_account_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "wallet_fund_recipients");
migrationBuilder.DropTable(
name: "wallet_funds");
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,160 +0,0 @@
using System;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddRealmFromSphere : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "realms",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
is_community = table.Column<bool>(type: "boolean", nullable: false),
is_public = table.Column<bool>(type: "boolean", nullable: false),
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_realms", x => x.id);
});
migrationBuilder.CreateTable(
name: "realm_members",
columns: table => new
{
realm_id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
role = table.Column<int>(type: "integer", nullable: false),
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_realm_members", x => new { x.realm_id, x.account_id });
table.ForeignKey(
name: "fk_realm_members_realms_realm_id",
column: x => x.realm_id,
principalTable: "realms",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "sn_chat_room",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
type = table.Column<int>(type: "integer", nullable: false),
is_community = table.Column<bool>(type: "boolean", nullable: false),
is_public = table.Column<bool>(type: "boolean", nullable: false),
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
realm_id = table.Column<Guid>(type: "uuid", nullable: true),
sn_realm_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_sn_chat_room", x => x.id);
table.ForeignKey(
name: "fk_sn_chat_room_realms_sn_realm_id",
column: x => x.sn_realm_id,
principalTable: "realms",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "sn_chat_member",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
chat_room_id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
role = table.Column<int>(type: "integer", nullable: false),
notify = table.Column<int>(type: "integer", nullable: false),
last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_bot = table.Column<bool>(type: "boolean", nullable: false),
break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_sn_chat_member", x => x.id);
table.ForeignKey(
name: "fk_sn_chat_member_sn_chat_room_chat_room_id",
column: x => x.chat_room_id,
principalTable: "sn_chat_room",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_realms_slug",
table: "realms",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_sn_chat_member_chat_room_id",
table: "sn_chat_member",
column: "chat_room_id");
migrationBuilder.CreateIndex(
name: "ix_sn_chat_room_sn_realm_id",
table: "sn_chat_room",
column: "sn_realm_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "realm_members");
migrationBuilder.DropTable(
name: "sn_chat_member");
migrationBuilder.DropTable(
name: "sn_chat_room");
migrationBuilder.DropTable(
name: "realms");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,99 +0,0 @@
using System;
using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class RemoveChatRoom : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "sn_chat_member");
migrationBuilder.DropTable(
name: "sn_chat_room");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "sn_chat_room",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
is_community = table.Column<bool>(type: "boolean", nullable: false),
is_public = table.Column<bool>(type: "boolean", nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
realm_id = table.Column<Guid>(type: "uuid", nullable: true),
sn_realm_id = table.Column<Guid>(type: "uuid", nullable: true),
type = table.Column<int>(type: "integer", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sn_chat_room", x => x.id);
table.ForeignKey(
name: "fk_sn_chat_room_realms_sn_realm_id",
column: x => x.sn_realm_id,
principalTable: "realms",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "sn_chat_member",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
chat_room_id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
break_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_bot = table.Column<bool>(type: "boolean", nullable: false),
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
last_read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
nick = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
notify = table.Column<int>(type: "integer", nullable: false),
role = table.Column<int>(type: "integer", nullable: false),
timeout_cause = table.Column<ChatTimeoutCause>(type: "jsonb", nullable: true),
timeout_until = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sn_chat_member", x => x.id);
table.ForeignKey(
name: "fk_sn_chat_member_sn_chat_room_chat_room_id",
column: x => x.chat_room_id,
principalTable: "sn_chat_room",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_sn_chat_member_chat_room_id",
table: "sn_chat_member",
column: "chat_room_id");
migrationBuilder.CreateIndex(
name: "ix_sn_chat_room_sn_realm_id",
table: "sn_chat_room",
column: "sn_realm_id");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +0,0 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddDetailLotteriesStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<List<int>>(
name: "matched_region_one_numbers",
table: "lotteries",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "matched_region_two_number",
table: "lotteries",
type: "integer",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "matched_region_one_numbers",
table: "lotteries");
migrationBuilder.DropColumn(
name: "matched_region_two_number",
table: "lotteries");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddPresenceActivity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "background_id",
table: "realms");
migrationBuilder.DropColumn(
name: "picture_id",
table: "realms");
migrationBuilder.CreateTable(
name: "presence_activities",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
manual_id = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
title = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
subtitle = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
caption = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
lease_minutes = table.Column<int>(type: "integer", nullable: false),
lease_expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_presence_activities", x => x.id);
table.ForeignKey(
name: "fk_presence_activities_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_presence_activities_account_id",
table: "presence_activities",
column: "account_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "presence_activities");
migrationBuilder.AddColumn<string>(
name: "background_id",
table: "realms",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "picture_id",
table: "realms",
type: "character varying(32)",
maxLength: 32,
nullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class EnrichPresenceActivity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "large_image",
table: "presence_activities",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "small_image",
table: "presence_activities",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "subtitle_url",
table: "presence_activities",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "title_url",
table: "presence_activities",
type: "character varying(4096)",
maxLength: 4096,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "large_image",
table: "presence_activities");
migrationBuilder.DropColumn(
name: "small_image",
table: "presence_activities");
migrationBuilder.DropColumn(
name: "subtitle_url",
table: "presence_activities");
migrationBuilder.DropColumn(
name: "title_url",
table: "presence_activities");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddSocialCreditRecordStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "status",
table: "social_credit_records",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "status",
table: "social_credit_records");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class OpenableFunds : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "is_open",
table: "wallet_funds",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<decimal>(
name: "remaining_amount",
table: "wallet_funds",
type: "numeric",
nullable: false,
defaultValue: 0m);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "is_open",
table: "wallet_funds");
migrationBuilder.DropColumn(
name: "remaining_amount",
table: "wallet_funds");
}
}
}

View File

@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class OpenFundsTotalSplits : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "amount_of_splits",
table: "wallet_funds",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "amount_of_splits",
table: "wallet_funds");
}
}
}

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Pass; using DysonNetwork.Pass;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -17,8 +17,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DysonNetwork.Pass.Migrations namespace DysonNetwork.Pass.Migrations
{ {
[DbContext(typeof(AppDatabase))] [DbContext(typeof(AppDatabase))]
[Migration("20251116163407_OpenFundsTotalSplits")] [Migration("20251214092550_InitialMigration")]
partial class OpenFundsTotalSplits partial class InitialMigration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -445,7 +445,7 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at"); .HasColumnName("last_seen_at");
b.Property<List<ProfileLink>>("Links") b.Property<List<SnProfileLink>>("Links")
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("links"); .HasColumnName("links");
@@ -715,6 +715,103 @@ namespace DysonNetwork.Pass.Migrations
b.ToTable("action_logs", (string)null); b.ToTable("action_logs", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("ResourceIdentifier")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("resource_identifier");
b.Property<Guid>("SpellId")
.HasColumnType("uuid")
.HasColumnName("spell_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_affiliation_results");
b.HasIndex("SpellId")
.HasDatabaseName("ix_affiliation_results_spell_id");
b.ToTable("affiliation_results", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationSpell", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid?>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant?>("AffectedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("affected_at");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<Dictionary<string, object>>("Meta")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("meta");
b.Property<string>("Spell")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("spell");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_affiliation_spells");
b.HasIndex("AccountId")
.HasDatabaseName("ix_affiliation_spells_account_id");
b.HasIndex("Spell")
.IsUnique()
.HasDatabaseName("ix_affiliation_spells_spell");
b.ToTable("affiliation_spells", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -781,10 +878,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("blacklist_factors"); .HasColumnName("blacklist_factors");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -793,6 +886,17 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at"); .HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("device_id");
b.Property<string>("DeviceName")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("device_name");
b.Property<Instant?>("ExpiredAt") b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expired_at"); .HasColumnName("expired_at");
@@ -815,6 +919,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
.HasColumnName("nonce"); .HasColumnName("nonce");
b.Property<int>("Platform")
.HasColumnType("integer")
.HasColumnName("platform");
b.Property<List<string>>("Scopes") b.Property<List<string>>("Scopes")
.IsRequired() .IsRequired()
.HasColumnType("jsonb") .HasColumnType("jsonb")
@@ -828,10 +936,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("integer") .HasColumnType("integer")
.HasColumnName("step_total"); .HasColumnName("step_total");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");
@@ -847,9 +951,6 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("AccountId") b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_challenges_account_id"); .HasDatabaseName("ix_auth_challenges_account_id");
b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_challenges_client_id");
b.ToTable("auth_challenges", (string)null); b.ToTable("auth_challenges", (string)null);
}); });
@@ -921,10 +1022,19 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("app_id"); .HasColumnName("app_id");
b.Property<List<string>>("Audiences")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("audiences");
b.Property<Guid?>("ChallengeId") b.Property<Guid?>("ChallengeId")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("challenge_id"); .HasColumnName("challenge_id");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -937,22 +1047,52 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expired_at"); .HasColumnName("expired_at");
b.Property<string>("IpAddress")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("ip_address");
b.Property<Instant?>("LastGrantedAt") b.Property<Instant?>("LastGrantedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_granted_at"); .HasColumnName("last_granted_at");
b.Property<GeoPoint>("Location")
.HasColumnType("jsonb")
.HasColumnName("location");
b.Property<Guid?>("ParentSessionId")
.HasColumnType("uuid")
.HasColumnName("parent_session_id");
b.Property<List<string>>("Scopes")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("scopes");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");
b.Property<string>("UserAgent")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("user_agent");
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_auth_sessions"); .HasName("pk_auth_sessions");
b.HasIndex("AccountId") b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_sessions_account_id"); .HasDatabaseName("ix_auth_sessions_account_id");
b.HasIndex("ChallengeId") b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_sessions_challenge_id"); .HasDatabaseName("ix_auth_sessions_client_id");
b.HasIndex("ParentSessionId")
.HasDatabaseName("ix_auth_sessions_parent_session_id");
b.ToTable("auth_sessions", (string)null); b.ToTable("auth_sessions", (string)null);
}); });
@@ -1317,12 +1457,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("affected_at"); .HasColumnName("affected_at");
b.Property<string>("Area")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("area");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -1345,6 +1479,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
.HasColumnName("key"); .HasColumnName("key");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");
@@ -1360,8 +1498,8 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("GroupId") b.HasIndex("GroupId")
.HasDatabaseName("ix_permission_nodes_group_id"); .HasDatabaseName("ix_permission_nodes_group_id");
b.HasIndex("Key", "Area", "Actor") b.HasIndex("Key", "Actor")
.HasDatabaseName("ix_permission_nodes_key_area_actor"); .HasDatabaseName("ix_permission_nodes_key_actor");
b.ToTable("permission_nodes", (string)null); b.ToTable("permission_nodes", (string)null);
}); });
@@ -2347,6 +2485,28 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Account"); b.Navigation("Account");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationResult", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnAffiliationSpell", "Spell")
.WithMany()
.HasForeignKey("SpellId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_affiliation_results_affiliation_spells_spell_id");
b.Navigation("Spell");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationSpell", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
.WithMany()
.HasForeignKey("AccountId")
.HasConstraintName("fk_affiliation_spells_accounts_account_id");
b.Navigation("Account");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
@@ -2377,14 +2537,7 @@ namespace DysonNetwork.Pass.Migrations
.IsRequired() .IsRequired()
.HasConstraintName("fk_auth_challenges_accounts_account_id"); .HasConstraintName("fk_auth_challenges_accounts_account_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.HasConstraintName("fk_auth_challenges_auth_clients_client_id");
b.Navigation("Account"); b.Navigation("Account");
b.Navigation("Client");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b =>
@@ -2408,14 +2561,21 @@ namespace DysonNetwork.Pass.Migrations
.IsRequired() .IsRequired()
.HasConstraintName("fk_auth_sessions_accounts_account_id"); .HasConstraintName("fk_auth_sessions_accounts_account_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge") b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
.WithMany() .WithMany()
.HasForeignKey("ChallengeId") .HasForeignKey("ClientId")
.HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); .HasConstraintName("fk_auth_sessions_auth_clients_client_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "ParentSession")
.WithMany()
.HasForeignKey("ParentSessionId")
.HasConstraintName("fk_auth_sessions_auth_sessions_parent_session_id");
b.Navigation("Account"); b.Navigation("Account");
b.Navigation("Challenge"); b.Navigation("Client");
b.Navigation("ParentSession");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>

View File

@@ -1,7 +1,9 @@
using System.Text.Json; using System;
using System.Collections.Generic;
using System.Text.Json;
using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using NetTopologySuite.Geometries;
using NodaTime; using NodaTime;
#nullable disable #nullable disable
@@ -14,9 +16,6 @@ namespace DysonNetwork.Pass.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "accounts", name: "accounts",
columns: table => new columns: table => new
@@ -25,8 +24,10 @@ namespace DysonNetwork.Pass.Migrations
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
nick = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), nick = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
language = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false), language = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
region = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
activated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), activated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_superuser = table.Column<bool>(type: "boolean", nullable: false), is_superuser = table.Column<bool>(type: "boolean", nullable: false),
automated_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
@@ -37,24 +38,23 @@ namespace DysonNetwork.Pass.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "custom_apps", name: "lottery_records",
columns: table => new columns: table => new
{ {
id = table.Column<Guid>(type: "uuid", nullable: false), id = table.Column<Guid>(type: "uuid", nullable: false),
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), winning_region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), winning_region_two_number = table.Column<int>(type: "integer", nullable: false),
status = table.Column<int>(type: "integer", nullable: false), total_tickets = table.Column<int>(type: "integer", nullable: false),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), total_prizes_awarded = table.Column<int>(type: "integer", nullable: false),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), total_prize_amount = table.Column<long>(type: "bigint", nullable: false),
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_custom_apps", x => x.id); table.PrimaryKey("pk_lottery_records", x => x.id);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -72,6 +72,29 @@ namespace DysonNetwork.Pass.Migrations
table.PrimaryKey("pk_permission_groups", x => x.id); table.PrimaryKey("pk_permission_groups", x => x.id);
}); });
migrationBuilder.CreateTable(
name: "realms",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
is_community = table.Column<bool>(type: "boolean", nullable: false),
is_public = table.Column<bool>(type: "boolean", nullable: false),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_realms", x => x.id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "wallet_coupons", name: "wallet_coupons",
columns: table => new columns: table => new
@@ -156,6 +179,7 @@ namespace DysonNetwork.Pass.Migrations
reward_experience = table.Column<int>(type: "integer", nullable: true), reward_experience = table.Column<int>(type: "integer", nullable: true),
tips = table.Column<ICollection<CheckInFortuneTip>>(type: "jsonb", nullable: false), tips = table.Column<ICollection<CheckInFortuneTip>>(type: "jsonb", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
backdated_from = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
@@ -206,6 +230,7 @@ namespace DysonNetwork.Pass.Migrations
type = table.Column<int>(type: "integer", nullable: false), type = table.Column<int>(type: "integer", nullable: false),
verified_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), verified_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_primary = table.Column<bool>(type: "boolean", nullable: false), is_primary = table.Column<bool>(type: "boolean", nullable: false),
is_public = table.Column<bool>(type: "boolean", nullable: false),
content = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), content = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -236,13 +261,14 @@ namespace DysonNetwork.Pass.Migrations
pronouns = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), pronouns = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
time_zone = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), time_zone = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
location = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), location = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
links = table.Column<List<SnProfileLink>>(type: "jsonb", nullable: true),
username_color = table.Column<UsernameColor>(type: "jsonb", nullable: true),
birthday = table.Column<Instant>(type: "timestamp with time zone", nullable: true), birthday = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
last_seen_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), last_seen_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true), verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
active_badge = table.Column<SnAccountBadge>(type: "jsonb", nullable: true), active_badge = table.Column<SnAccountBadgeRef>(type: "jsonb", nullable: true),
experience = table.Column<int>(type: "integer", nullable: false), experience = table.Column<int>(type: "integer", nullable: false),
picture_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true), social_credits = table.Column<double>(type: "double precision", nullable: false),
background_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true), background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
@@ -299,7 +325,10 @@ namespace DysonNetwork.Pass.Migrations
is_invisible = table.Column<bool>(type: "boolean", nullable: false), is_invisible = table.Column<bool>(type: "boolean", nullable: false),
is_not_disturb = table.Column<bool>(type: "boolean", nullable: false), is_not_disturb = table.Column<bool>(type: "boolean", nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
cleared_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), cleared_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
app_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
is_automated = table.Column<bool>(type: "boolean", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -325,7 +354,7 @@ namespace DysonNetwork.Pass.Migrations
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false), meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true), user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true), ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
location = table.Column<Point>(type: "geometry", nullable: true), location = table.Column<GeoPoint>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
session_id = table.Column<Guid>(type: "uuid", nullable: true), session_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -343,6 +372,31 @@ namespace DysonNetwork.Pass.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "affiliation_spells",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
spell = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
affected_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_affiliation_spells", x => x.id);
table.ForeignKey(
name: "fk_affiliation_spells_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id");
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "auth_challenges", name: "auth_challenges",
columns: table => new columns: table => new
@@ -352,16 +406,16 @@ namespace DysonNetwork.Pass.Migrations
step_remain = table.Column<int>(type: "integer", nullable: false), step_remain = table.Column<int>(type: "integer", nullable: false),
step_total = table.Column<int>(type: "integer", nullable: false), step_total = table.Column<int>(type: "integer", nullable: false),
failed_attempts = table.Column<int>(type: "integer", nullable: false), failed_attempts = table.Column<int>(type: "integer", nullable: false),
platform = table.Column<int>(type: "integer", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
blacklist_factors = table.Column<List<Guid>>(type: "jsonb", nullable: false), blacklist_factors = table.Column<List<Guid>>(type: "jsonb", nullable: false),
audiences = table.Column<List<string>>(type: "jsonb", nullable: false), audiences = table.Column<List<string>>(type: "jsonb", nullable: false),
scopes = table.Column<List<string>>(type: "jsonb", nullable: false), scopes = table.Column<List<string>>(type: "jsonb", nullable: false),
ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true), ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true), user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
device_id = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), device_id = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
device_name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
platform = table.Column<int>(type: "integer", nullable: false),
nonce = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), nonce = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
location = table.Column<Point>(type: "geometry", nullable: true), location = table.Column<GeoPoint>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -378,6 +432,31 @@ namespace DysonNetwork.Pass.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "auth_clients",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
platform = table.Column<int>(type: "integer", nullable: false),
device_name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
device_label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
device_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_auth_clients", x => x.id);
table.ForeignKey(
name: "fk_auth_clients_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "badges", name: "badges",
columns: table => new columns: table => new
@@ -405,6 +484,59 @@ namespace DysonNetwork.Pass.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "experience_records",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
reason_type = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
reason = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
delta = table.Column<long>(type: "bigint", nullable: false),
bonus_multiplier = table.Column<double>(type: "double precision", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_experience_records", x => x.id);
table.ForeignKey(
name: "fk_experience_records_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "lotteries",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
region_two_number = table.Column<int>(type: "integer", nullable: false),
multiplier = table.Column<int>(type: "integer", nullable: false),
draw_status = table.Column<int>(type: "integer", nullable: false),
draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
matched_region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: true),
matched_region_two_number = table.Column<int>(type: "integer", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_lotteries", x => x.id);
table.ForeignKey(
name: "fk_lotteries_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "magic_spells", name: "magic_spells",
columns: table => new columns: table => new
@@ -431,14 +563,22 @@ namespace DysonNetwork.Pass.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "notification_push_subscriptions", name: "presence_activities",
columns: table => new columns: table => new
{ {
id = table.Column<Guid>(type: "uuid", nullable: false), id = table.Column<Guid>(type: "uuid", nullable: false),
device_id = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false), type = table.Column<int>(type: "integer", nullable: false),
device_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false), manual_id = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
provider = table.Column<int>(type: "integer", nullable: false), title = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), subtitle = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
caption = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
large_image = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
small_image = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
title_url = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
subtitle_url = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
lease_minutes = table.Column<int>(type: "integer", nullable: false),
lease_expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -446,9 +586,9 @@ namespace DysonNetwork.Pass.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_notification_push_subscriptions", x => x.id); table.PrimaryKey("pk_presence_activities", x => x.id);
table.ForeignKey( table.ForeignKey(
name: "fk_notification_push_subscriptions_accounts_account_id", name: "fk_presence_activities_accounts_account_id",
column: x => x.account_id, column: x => x.account_id,
principalTable: "accounts", principalTable: "accounts",
principalColumn: "id", principalColumn: "id",
@@ -456,17 +596,14 @@ namespace DysonNetwork.Pass.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "notifications", name: "punishments",
columns: table => new columns: table => new
{ {
id = table.Column<Guid>(type: "uuid", nullable: false), id = table.Column<Guid>(type: "uuid", nullable: false),
topic = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), reason = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
title = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
subtitle = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true), type = table.Column<int>(type: "integer", nullable: false),
content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), blocked_permissions = table.Column<List<string>>(type: "jsonb", nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
priority = table.Column<int>(type: "integer", nullable: false),
viewed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -474,15 +611,71 @@ namespace DysonNetwork.Pass.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("pk_notifications", x => x.id); table.PrimaryKey("pk_punishments", x => x.id);
table.ForeignKey( table.ForeignKey(
name: "fk_notifications_accounts_account_id", name: "fk_punishments_accounts_account_id",
column: x => x.account_id, column: x => x.account_id,
principalTable: "accounts", principalTable: "accounts",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "social_credit_records",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
reason_type = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
reason = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
delta = table.Column<double>(type: "double precision", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_social_credit_records", x => x.id);
table.ForeignKey(
name: "fk_social_credit_records_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "wallet_funds",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
total_amount = table.Column<decimal>(type: "numeric", nullable: false),
remaining_amount = table.Column<decimal>(type: "numeric", nullable: false),
amount_of_splits = table.Column<int>(type: "integer", nullable: false),
split_type = table.Column<int>(type: "integer", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
message = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
is_open = table.Column<bool>(type: "boolean", nullable: false),
creator_account_id = table.Column<Guid>(type: "uuid", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_funds", x => x.id);
table.ForeignKey(
name: "fk_wallet_funds_accounts_creator_account_id",
column: x => x.creator_account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "wallets", name: "wallets",
columns: table => new columns: table => new
@@ -504,31 +697,6 @@ namespace DysonNetwork.Pass.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "custom_app_secrets",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
secret = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
is_oidc = table.Column<bool>(type: "boolean", nullable: false),
app_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_custom_app_secrets", x => x.id);
table.ForeignKey(
name: "fk_custom_app_secrets_custom_apps_app_id",
column: x => x.app_id,
principalTable: "custom_apps",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "permission_group_members", name: "permission_group_members",
columns: table => new columns: table => new
@@ -557,8 +725,8 @@ namespace DysonNetwork.Pass.Migrations
columns: table => new columns: table => new
{ {
id = table.Column<Guid>(type: "uuid", nullable: false), id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
actor = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), actor = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
area = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
key = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), key = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
value = table.Column<JsonDocument>(type: "jsonb", nullable: false), value = table.Column<JsonDocument>(type: "jsonb", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
@@ -578,6 +746,30 @@ namespace DysonNetwork.Pass.Migrations
principalColumn: "id"); principalColumn: "id");
}); });
migrationBuilder.CreateTable(
name: "realm_members",
columns: table => new
{
realm_id = table.Column<Guid>(type: "uuid", nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
role = table.Column<int>(type: "integer", nullable: false),
joined_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
leave_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_realm_members", x => new { x.realm_id, x.account_id });
table.ForeignKey(
name: "fk_realm_members_realms_realm_id",
column: x => x.realm_id,
principalTable: "realms",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "wallet_subscriptions", name: "wallet_subscriptions",
columns: table => new columns: table => new
@@ -615,16 +807,45 @@ namespace DysonNetwork.Pass.Migrations
principalColumn: "id"); principalColumn: "id");
}); });
migrationBuilder.CreateTable(
name: "affiliation_results",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
resource_identifier = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
spell_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_affiliation_results", x => x.id);
table.ForeignKey(
name: "fk_affiliation_results_affiliation_spells_spell_id",
column: x => x.spell_id,
principalTable: "affiliation_spells",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "auth_sessions", name: "auth_sessions",
columns: table => new columns: table => new
{ {
id = table.Column<Guid>(type: "uuid", nullable: false), id = table.Column<Guid>(type: "uuid", nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), type = table.Column<int>(type: "integer", nullable: false),
last_granted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), last_granted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
audiences = table.Column<List<string>>(type: "jsonb", nullable: false),
scopes = table.Column<List<string>>(type: "jsonb", nullable: false),
ip_address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
user_agent = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
location = table.Column<GeoPoint>(type: "jsonb", nullable: true),
account_id = table.Column<Guid>(type: "uuid", nullable: false), account_id = table.Column<Guid>(type: "uuid", nullable: false),
challenge_id = table.Column<Guid>(type: "uuid", nullable: false), client_id = table.Column<Guid>(type: "uuid", nullable: true),
parent_session_id = table.Column<Guid>(type: "uuid", nullable: true),
challenge_id = table.Column<Guid>(type: "uuid", nullable: true),
app_id = table.Column<Guid>(type: "uuid", nullable: true), app_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -640,16 +861,46 @@ namespace DysonNetwork.Pass.Migrations
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_auth_sessions_auth_challenges_challenge_id", name: "fk_auth_sessions_auth_clients_client_id",
column: x => x.challenge_id, column: x => x.client_id,
principalTable: "auth_challenges", principalTable: "auth_clients",
principalColumn: "id");
table.ForeignKey(
name: "fk_auth_sessions_auth_sessions_parent_session_id",
column: x => x.parent_session_id,
principalTable: "auth_sessions",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "wallet_fund_recipients",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
fund_id = table.Column<Guid>(type: "uuid", nullable: false),
recipient_account_id = table.Column<Guid>(type: "uuid", nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false),
is_received = table.Column<bool>(type: "boolean", nullable: false),
received_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_fund_recipients", x => x.id);
table.ForeignKey(
name: "fk_wallet_fund_recipients_accounts_recipient_account_id",
column: x => x.recipient_account_id,
principalTable: "accounts",
principalColumn: "id", principalColumn: "id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey( table.ForeignKey(
name: "fk_auth_sessions_custom_apps_app_id", name: "fk_wallet_fund_recipients_wallet_funds_fund_id",
column: x => x.app_id, column: x => x.fund_id,
principalTable: "custom_apps", principalTable: "wallet_funds",
principalColumn: "id"); principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -705,6 +956,91 @@ namespace DysonNetwork.Pass.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "wallet_gifts",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
gifter_id = table.Column<Guid>(type: "uuid", nullable: false),
recipient_id = table.Column<Guid>(type: "uuid", nullable: true),
gift_code = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
message = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
subscription_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
base_price = table.Column<decimal>(type: "numeric", nullable: false),
final_price = table.Column<decimal>(type: "numeric", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
redeemed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
redeemer_id = table.Column<Guid>(type: "uuid", nullable: true),
subscription_id = table.Column<Guid>(type: "uuid", nullable: true),
expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
is_open_gift = table.Column<bool>(type: "boolean", nullable: false),
payment_method = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
payment_details = table.Column<SnPaymentDetails>(type: "jsonb", nullable: false),
coupon_id = table.Column<Guid>(type: "uuid", nullable: true),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_wallet_gifts", x => x.id);
table.ForeignKey(
name: "fk_wallet_gifts_accounts_gifter_id",
column: x => x.gifter_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_wallet_gifts_accounts_recipient_id",
column: x => x.recipient_id,
principalTable: "accounts",
principalColumn: "id");
table.ForeignKey(
name: "fk_wallet_gifts_accounts_redeemer_id",
column: x => x.redeemer_id,
principalTable: "accounts",
principalColumn: "id");
table.ForeignKey(
name: "fk_wallet_gifts_wallet_coupons_coupon_id",
column: x => x.coupon_id,
principalTable: "wallet_coupons",
principalColumn: "id");
table.ForeignKey(
name: "fk_wallet_gifts_wallet_subscriptions_subscription_id",
column: x => x.subscription_id,
principalTable: "wallet_subscriptions",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "api_keys",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
account_id = table.Column<Guid>(type: "uuid", nullable: false),
session_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_api_keys", x => x.id);
table.ForeignKey(
name: "fk_api_keys_accounts_account_id",
column: x => x.account_id,
principalTable: "accounts",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_api_keys_auth_sessions_session_id",
column: x => x.session_id,
principalTable: "auth_sessions",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "payment_orders", name: "payment_orders",
columns: table => new columns: table => new
@@ -714,6 +1050,7 @@ namespace DysonNetwork.Pass.Migrations
currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false), currency = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
remarks = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), remarks = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
app_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true), app_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
product_identifier = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
amount = table.Column<decimal>(type: "numeric", nullable: false), amount = table.Column<decimal>(type: "numeric", nullable: false),
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
@@ -790,25 +1127,56 @@ namespace DysonNetwork.Pass.Migrations
table: "action_logs", table: "action_logs",
column: "account_id"); column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_affiliation_results_spell_id",
table: "affiliation_results",
column: "spell_id");
migrationBuilder.CreateIndex(
name: "ix_affiliation_spells_account_id",
table: "affiliation_spells",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_affiliation_spells_spell",
table: "affiliation_spells",
column: "spell",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_api_keys_account_id",
table: "api_keys",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_api_keys_session_id",
table: "api_keys",
column: "session_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_auth_challenges_account_id", name: "ix_auth_challenges_account_id",
table: "auth_challenges", table: "auth_challenges",
column: "account_id"); column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_auth_clients_account_id",
table: "auth_clients",
column: "account_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_auth_sessions_account_id", name: "ix_auth_sessions_account_id",
table: "auth_sessions", table: "auth_sessions",
column: "account_id"); column: "account_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_auth_sessions_app_id", name: "ix_auth_sessions_client_id",
table: "auth_sessions", table: "auth_sessions",
column: "app_id"); column: "client_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_auth_sessions_challenge_id", name: "ix_auth_sessions_parent_session_id",
table: "auth_sessions", table: "auth_sessions",
column: "challenge_id"); column: "parent_session_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_badges_account_id", name: "ix_badges_account_id",
@@ -816,9 +1184,14 @@ namespace DysonNetwork.Pass.Migrations
column: "account_id"); column: "account_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_custom_app_secrets_app_id", name: "ix_experience_records_account_id",
table: "custom_app_secrets", table: "experience_records",
column: "app_id"); column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_lotteries_account_id",
table: "lotteries",
column: "account_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_magic_spells_account_id", name: "ix_magic_spells_account_id",
@@ -831,22 +1204,6 @@ namespace DysonNetwork.Pass.Migrations
column: "spell", column: "spell",
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "ix_notification_push_subscriptions_account_id",
table: "notification_push_subscriptions",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_notification_push_subscriptions_device_token_device_id_acco",
table: "notification_push_subscriptions",
columns: new[] { "device_token", "device_id", "account_id" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_notifications_account_id",
table: "notifications",
column: "account_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_payment_orders_payee_wallet_id", name: "ix_payment_orders_payee_wallet_id",
table: "payment_orders", table: "payment_orders",
@@ -873,9 +1230,76 @@ namespace DysonNetwork.Pass.Migrations
column: "group_id"); column: "group_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_permission_nodes_key_area_actor", name: "ix_permission_nodes_key_actor",
table: "permission_nodes", table: "permission_nodes",
columns: new[] { "key", "area", "actor" }); columns: new[] { "key", "actor" });
migrationBuilder.CreateIndex(
name: "ix_presence_activities_account_id",
table: "presence_activities",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_punishments_account_id",
table: "punishments",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_realms_slug",
table: "realms",
column: "slug",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_social_credit_records_account_id",
table: "social_credit_records",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_fund_recipients_fund_id",
table: "wallet_fund_recipients",
column: "fund_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_fund_recipients_recipient_account_id",
table: "wallet_fund_recipients",
column: "recipient_account_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_funds_creator_account_id",
table: "wallet_funds",
column: "creator_account_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_coupon_id",
table: "wallet_gifts",
column: "coupon_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_gift_code",
table: "wallet_gifts",
column: "gift_code");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_gifter_id",
table: "wallet_gifts",
column: "gifter_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_recipient_id",
table: "wallet_gifts",
column: "recipient_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_redeemer_id",
table: "wallet_gifts",
column: "redeemer_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_gifts_subscription_id",
table: "wallet_gifts",
column: "subscription_id",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_wallet_pockets_wallet_id", name: "ix_wallet_pockets_wallet_id",
@@ -887,6 +1311,16 @@ namespace DysonNetwork.Pass.Migrations
table: "wallet_subscriptions", table: "wallet_subscriptions",
column: "account_id"); column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_account_id_identifier",
table: "wallet_subscriptions",
columns: new[] { "account_id", "identifier" });
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_account_id_is_active",
table: "wallet_subscriptions",
columns: new[] { "account_id", "is_active" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_coupon_id", name: "ix_wallet_subscriptions_coupon_id",
table: "wallet_subscriptions", table: "wallet_subscriptions",
@@ -897,6 +1331,11 @@ namespace DysonNetwork.Pass.Migrations
table: "wallet_subscriptions", table: "wallet_subscriptions",
column: "identifier"); column: "identifier");
migrationBuilder.CreateIndex(
name: "ix_wallet_subscriptions_status",
table: "wallet_subscriptions",
column: "status");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_wallets_account_id", name: "ix_wallets_account_id",
table: "wallets", table: "wallets",
@@ -934,23 +1373,29 @@ namespace DysonNetwork.Pass.Migrations
name: "action_logs"); name: "action_logs");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "auth_sessions"); name: "affiliation_results");
migrationBuilder.DropTable(
name: "api_keys");
migrationBuilder.DropTable(
name: "auth_challenges");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "badges"); name: "badges");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "custom_app_secrets"); name: "experience_records");
migrationBuilder.DropTable(
name: "lotteries");
migrationBuilder.DropTable(
name: "lottery_records");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "magic_spells"); name: "magic_spells");
migrationBuilder.DropTable(
name: "notification_push_subscriptions");
migrationBuilder.DropTable(
name: "notifications");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "payment_orders"); name: "payment_orders");
@@ -960,17 +1405,32 @@ namespace DysonNetwork.Pass.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "permission_nodes"); name: "permission_nodes");
migrationBuilder.DropTable(
name: "presence_activities");
migrationBuilder.DropTable(
name: "punishments");
migrationBuilder.DropTable(
name: "realm_members");
migrationBuilder.DropTable(
name: "social_credit_records");
migrationBuilder.DropTable(
name: "wallet_fund_recipients");
migrationBuilder.DropTable(
name: "wallet_gifts");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "wallet_pockets"); name: "wallet_pockets");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "wallet_subscriptions"); name: "affiliation_spells");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "auth_challenges"); name: "auth_sessions");
migrationBuilder.DropTable(
name: "custom_apps");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "payment_transactions"); name: "payment_transactions");
@@ -979,11 +1439,23 @@ namespace DysonNetwork.Pass.Migrations
name: "permission_groups"); name: "permission_groups");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "wallet_coupons"); name: "realms");
migrationBuilder.DropTable(
name: "wallet_funds");
migrationBuilder.DropTable(
name: "wallet_subscriptions");
migrationBuilder.DropTable(
name: "auth_clients");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "wallets"); name: "wallets");
migrationBuilder.DropTable(
name: "wallet_coupons");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "accounts"); name: "accounts");
} }

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Pass; using DysonNetwork.Pass;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -442,7 +442,7 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at"); .HasColumnName("last_seen_at");
b.Property<List<ProfileLink>>("Links") b.Property<List<SnProfileLink>>("Links")
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("links"); .HasColumnName("links");
@@ -712,6 +712,103 @@ namespace DysonNetwork.Pass.Migrations
b.ToTable("action_logs", (string)null); b.ToTable("action_logs", (string)null);
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationResult", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<string>("ResourceIdentifier")
.IsRequired()
.HasMaxLength(8192)
.HasColumnType("character varying(8192)")
.HasColumnName("resource_identifier");
b.Property<Guid>("SpellId")
.HasColumnType("uuid")
.HasColumnName("spell_id");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_affiliation_results");
b.HasIndex("SpellId")
.HasDatabaseName("ix_affiliation_results_spell_id");
b.ToTable("affiliation_results", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationSpell", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<Guid?>("AccountId")
.HasColumnType("uuid")
.HasColumnName("account_id");
b.Property<Instant?>("AffectedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("affected_at");
b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Instant?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
b.Property<Instant?>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<Dictionary<string, object>>("Meta")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("meta");
b.Property<string>("Spell")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("spell");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id")
.HasName("pk_affiliation_spells");
b.HasIndex("AccountId")
.HasDatabaseName("ix_affiliation_spells_account_id");
b.HasIndex("Spell")
.IsUnique()
.HasDatabaseName("ix_affiliation_spells_spell");
b.ToTable("affiliation_spells", (string)null);
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -778,10 +875,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("jsonb") .HasColumnType("jsonb")
.HasColumnName("blacklist_factors"); .HasColumnName("blacklist_factors");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -790,6 +883,17 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at"); .HasColumnName("deleted_at");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("device_id");
b.Property<string>("DeviceName")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("device_name");
b.Property<Instant?>("ExpiredAt") b.Property<Instant?>("ExpiredAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expired_at"); .HasColumnName("expired_at");
@@ -812,6 +916,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
.HasColumnName("nonce"); .HasColumnName("nonce");
b.Property<int>("Platform")
.HasColumnType("integer")
.HasColumnName("platform");
b.Property<List<string>>("Scopes") b.Property<List<string>>("Scopes")
.IsRequired() .IsRequired()
.HasColumnType("jsonb") .HasColumnType("jsonb")
@@ -825,10 +933,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("integer") .HasColumnType("integer")
.HasColumnName("step_total"); .HasColumnName("step_total");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");
@@ -844,9 +948,6 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("AccountId") b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_challenges_account_id"); .HasDatabaseName("ix_auth_challenges_account_id");
b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_challenges_client_id");
b.ToTable("auth_challenges", (string)null); b.ToTable("auth_challenges", (string)null);
}); });
@@ -918,10 +1019,19 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("app_id"); .HasColumnName("app_id");
b.Property<List<string>>("Audiences")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("audiences");
b.Property<Guid?>("ChallengeId") b.Property<Guid?>("ChallengeId")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasColumnName("challenge_id"); .HasColumnName("challenge_id");
b.Property<Guid?>("ClientId")
.HasColumnType("uuid")
.HasColumnName("client_id");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -934,22 +1044,52 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expired_at"); .HasColumnName("expired_at");
b.Property<string>("IpAddress")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("ip_address");
b.Property<Instant?>("LastGrantedAt") b.Property<Instant?>("LastGrantedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_granted_at"); .HasColumnName("last_granted_at");
b.Property<GeoPoint>("Location")
.HasColumnType("jsonb")
.HasColumnName("location");
b.Property<Guid?>("ParentSessionId")
.HasColumnType("uuid")
.HasColumnName("parent_session_id");
b.Property<List<string>>("Scopes")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("scopes");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");
b.Property<string>("UserAgent")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("user_agent");
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_auth_sessions"); .HasName("pk_auth_sessions");
b.HasIndex("AccountId") b.HasIndex("AccountId")
.HasDatabaseName("ix_auth_sessions_account_id"); .HasDatabaseName("ix_auth_sessions_account_id");
b.HasIndex("ChallengeId") b.HasIndex("ClientId")
.HasDatabaseName("ix_auth_sessions_challenge_id"); .HasDatabaseName("ix_auth_sessions_client_id");
b.HasIndex("ParentSessionId")
.HasDatabaseName("ix_auth_sessions_parent_session_id");
b.ToTable("auth_sessions", (string)null); b.ToTable("auth_sessions", (string)null);
}); });
@@ -1314,12 +1454,6 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("affected_at"); .HasColumnName("affected_at");
b.Property<string>("Area")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("area");
b.Property<Instant>("CreatedAt") b.Property<Instant>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -1342,6 +1476,10 @@ namespace DysonNetwork.Pass.Migrations
.HasColumnType("character varying(1024)") .HasColumnType("character varying(1024)")
.HasColumnName("key"); .HasColumnName("key");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Instant>("UpdatedAt") b.Property<Instant>("UpdatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("updated_at"); .HasColumnName("updated_at");
@@ -1357,8 +1495,8 @@ namespace DysonNetwork.Pass.Migrations
b.HasIndex("GroupId") b.HasIndex("GroupId")
.HasDatabaseName("ix_permission_nodes_group_id"); .HasDatabaseName("ix_permission_nodes_group_id");
b.HasIndex("Key", "Area", "Actor") b.HasIndex("Key", "Actor")
.HasDatabaseName("ix_permission_nodes_key_area_actor"); .HasDatabaseName("ix_permission_nodes_key_actor");
b.ToTable("permission_nodes", (string)null); b.ToTable("permission_nodes", (string)null);
}); });
@@ -2344,6 +2482,28 @@ namespace DysonNetwork.Pass.Migrations
b.Navigation("Account"); b.Navigation("Account");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationResult", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnAffiliationSpell", "Spell")
.WithMany()
.HasForeignKey("SpellId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_affiliation_results_affiliation_spells_spell_id");
b.Navigation("Spell");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAffiliationSpell", b =>
{
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
.WithMany()
.HasForeignKey("AccountId")
.HasConstraintName("fk_affiliation_spells_accounts_account_id");
b.Navigation("Account");
});
modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnApiKey", b =>
{ {
b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account") b.HasOne("DysonNetwork.Shared.Models.SnAccount", "Account")
@@ -2374,14 +2534,7 @@ namespace DysonNetwork.Pass.Migrations
.IsRequired() .IsRequired()
.HasConstraintName("fk_auth_challenges_accounts_account_id"); .HasConstraintName("fk_auth_challenges_accounts_account_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.HasConstraintName("fk_auth_challenges_auth_clients_client_id");
b.Navigation("Account"); b.Navigation("Account");
b.Navigation("Client");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnAuthClient", b =>
@@ -2405,14 +2558,21 @@ namespace DysonNetwork.Pass.Migrations
.IsRequired() .IsRequired()
.HasConstraintName("fk_auth_sessions_accounts_account_id"); .HasConstraintName("fk_auth_sessions_accounts_account_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthChallenge", "Challenge") b.HasOne("DysonNetwork.Shared.Models.SnAuthClient", "Client")
.WithMany() .WithMany()
.HasForeignKey("ChallengeId") .HasForeignKey("ClientId")
.HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); .HasConstraintName("fk_auth_sessions_auth_clients_client_id");
b.HasOne("DysonNetwork.Shared.Models.SnAuthSession", "ParentSession")
.WithMany()
.HasForeignKey("ParentSessionId")
.HasConstraintName("fk_auth_sessions_auth_sessions_parent_session_id");
b.Navigation("Account"); b.Navigation("Account");
b.Navigation("Challenge"); b.Navigation("Client");
b.Navigation("ParentSession");
}); });
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b => modelBuilder.Entity("DysonNetwork.Shared.Models.SnCheckInResult", b =>

View File

@@ -1,17 +1,12 @@
using DysonNetwork.Shared.Auth;
namespace DysonNetwork.Pass.Permission; namespace DysonNetwork.Pass.Permission;
using System; using System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using DysonNetwork.Shared.Models; using Shared.Models;
[AttributeUsage(AttributeTargets.Method)] public class LocalPermissionMiddleware(RequestDelegate next, ILogger<LocalPermissionMiddleware> logger)
public class RequiredPermissionAttribute(string area, string key) : Attribute
{
public string Area { get; set; } = area;
public string Key { get; } = key;
}
public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddleware> logger)
{ {
private const string ForbiddenMessage = "Insufficient permissions"; private const string ForbiddenMessage = "Insufficient permissions";
private const string UnauthorizedMessage = "Authentication required"; private const string UnauthorizedMessage = "Authentication required";
@@ -21,15 +16,15 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
var endpoint = httpContext.GetEndpoint(); var endpoint = httpContext.GetEndpoint();
var attr = endpoint?.Metadata var attr = endpoint?.Metadata
.OfType<RequiredPermissionAttribute>() .OfType<AskPermissionAttribute>()
.FirstOrDefault(); .FirstOrDefault();
if (attr != null) if (attr != null)
{ {
// Validate permission attributes // Validate permission attributes
if (string.IsNullOrWhiteSpace(attr.Area) || string.IsNullOrWhiteSpace(attr.Key)) if (string.IsNullOrWhiteSpace(attr.Key))
{ {
logger.LogWarning("Invalid permission attribute: Area='{Area}', Key='{Key}'", attr.Area, attr.Key); logger.LogWarning("Invalid permission attribute: Key='{Key}'", attr.Key);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsync("Server configuration error"); await httpContext.Response.WriteAsync("Server configuration error");
return; return;
@@ -37,7 +32,7 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
if (httpContext.Items["CurrentUser"] is not SnAccount currentUser) if (httpContext.Items["CurrentUser"] is not SnAccount currentUser)
{ {
logger.LogWarning("Permission check failed: No authenticated user for {Area}/{Key}", attr.Area, attr.Key); logger.LogWarning("Permission check failed: No authenticated user for {Key}", attr.Key);
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
await httpContext.Response.WriteAsync(UnauthorizedMessage); await httpContext.Response.WriteAsync(UnauthorizedMessage);
return; return;
@@ -46,33 +41,29 @@ public class PermissionMiddleware(RequestDelegate next, ILogger<PermissionMiddle
if (currentUser.IsSuperuser) if (currentUser.IsSuperuser)
{ {
// Bypass the permission check for performance // Bypass the permission check for performance
logger.LogDebug("Superuser {UserId} bypassing permission check for {Area}/{Key}", logger.LogDebug("Superuser {UserId} bypassing permission check for {Key}", currentUser.Id, attr.Key);
currentUser.Id, attr.Area, attr.Key);
await next(httpContext); await next(httpContext);
return; return;
} }
var actor = $"user:{currentUser.Id}"; var actor = currentUser.Id.ToString();
try try
{ {
var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Area, attr.Key); var permNode = await pm.GetPermissionAsync<bool>(actor, attr.Key);
if (!permNode) if (!permNode)
{ {
logger.LogWarning("Permission denied for user {UserId}: {Area}/{Key}", logger.LogWarning("Permission denied for user {UserId}: {Key}", currentUser.Id, attr.Key);
currentUser.Id, attr.Area, attr.Key);
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
await httpContext.Response.WriteAsync(ForbiddenMessage); await httpContext.Response.WriteAsync(ForbiddenMessage);
return; return;
} }
logger.LogDebug("Permission granted for user {UserId}: {Area}/{Key}", logger.LogDebug("Permission granted for user {UserId}: {Key}", currentUser.Id, attr.Key);
currentUser.Id, attr.Area, attr.Key);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error checking permission for user {UserId}: {Area}/{Key}", logger.LogError(ex, "Error checking permission for user {UserId}: {Key}", currentUser.Id, attr.Key);
currentUser.Id, attr.Area, attr.Key);
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsync("Permission check failed"); await httpContext.Response.WriteAsync("Permission check failed");
return; return;

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
using NodaTime; using NodaTime;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
namespace DysonNetwork.Pass.Permission; namespace DysonNetwork.Pass.Permission;
@@ -28,8 +29,8 @@ public class PermissionService(
private const string PermissionGroupCacheKeyPrefix = "perm-cg:"; private const string PermissionGroupCacheKeyPrefix = "perm-cg:";
private const string PermissionGroupPrefix = "perm-g:"; private const string PermissionGroupPrefix = "perm-g:";
private static string GetPermissionCacheKey(string actor, string area, string key) => private static string GetPermissionCacheKey(string actor, string key) =>
PermissionCacheKeyPrefix + actor + ":" + area + ":" + key; PermissionCacheKeyPrefix + actor + ":" + key;
private static string GetGroupsCacheKey(string actor) => private static string GetGroupsCacheKey(string actor) =>
PermissionGroupCacheKeyPrefix + actor; PermissionGroupCacheKeyPrefix + actor;
@@ -37,50 +38,56 @@ public class PermissionService(
private static string GetPermissionGroupKey(string actor) => private static string GetPermissionGroupKey(string actor) =>
PermissionGroupPrefix + actor; PermissionGroupPrefix + actor;
public async Task<bool> HasPermissionAsync(string actor, string area, string key) public async Task<bool> HasPermissionAsync(
string actor,
string key,
PermissionNodeActorType type = PermissionNodeActorType.Account
)
{ {
var value = await GetPermissionAsync<bool>(actor, area, key); var value = await GetPermissionAsync<bool>(actor, key, type);
return value; return value;
} }
public async Task<T?> GetPermissionAsync<T>(string actor, string area, string key) public async Task<T?> GetPermissionAsync<T>(
string actor,
string key,
PermissionNodeActorType type = PermissionNodeActorType.Account
)
{ {
// Input validation // Input validation
if (string.IsNullOrWhiteSpace(actor)) if (string.IsNullOrWhiteSpace(actor))
throw new ArgumentException("Actor cannot be null or empty", nameof(actor)); throw new ArgumentException("Actor cannot be null or empty", nameof(actor));
if (string.IsNullOrWhiteSpace(area))
throw new ArgumentException("Area cannot be null or empty", nameof(area));
if (string.IsNullOrWhiteSpace(key)) if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Key cannot be null or empty", nameof(key)); throw new ArgumentException("Key cannot be null or empty", nameof(key));
var cacheKey = GetPermissionCacheKey(actor, area, key); var cacheKey = GetPermissionCacheKey(actor, key);
try try
{ {
var (hit, cachedValue) = await cache.GetAsyncWithStatus<T>(cacheKey); var (hit, cachedValue) = await cache.GetAsyncWithStatus<T>(cacheKey);
if (hit) if (hit)
{ {
logger.LogDebug("Permission cache hit for {Actor}:{Area}:{Key}", actor, area, key); logger.LogDebug("Permission cache hit for {Type}:{Actor}:{Key}", type, actor, key);
return cachedValue; return cachedValue;
} }
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var groupsId = await GetOrCacheUserGroupsAsync(actor, now); var groupsId = await GetOrCacheUserGroupsAsync(actor, now);
var permission = await FindPermissionNodeAsync(actor, area, key, groupsId, now); var permission = await FindPermissionNodeAsync(type, actor, key, groupsId);
var result = permission != null ? DeserializePermissionValue<T>(permission.Value) : default; var result = permission != null ? DeserializePermissionValue<T>(permission.Value) : default;
await cache.SetWithGroupsAsync(cacheKey, result, await cache.SetWithGroupsAsync(cacheKey, result,
[GetPermissionGroupKey(actor)], [GetPermissionGroupKey(actor)],
_options.CacheExpiration); _options.CacheExpiration);
logger.LogDebug("Permission resolved for {Actor}:{Area}:{Key} = {Result}", logger.LogDebug("Permission resolved for {Type}:{Actor}:{Key} = {Result}", type, actor, key,
actor, area, key, result != null); result != null);
return result; return result;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error retrieving permission for {Actor}:{Area}:{Key}", actor, area, key); logger.LogError(ex, "Error retrieving permission for {Type}:{Actor}:{Key}", type, actor, key);
throw; throw;
} }
} }
@@ -109,33 +116,34 @@ public class PermissionService(
return groupsId; return groupsId;
} }
private async Task<SnPermissionNode?> FindPermissionNodeAsync(string actor, string area, string key, private async Task<SnPermissionNode?> FindPermissionNodeAsync(
List<Guid> groupsId, Instant now) PermissionNodeActorType type,
string actor,
string key,
List<Guid> groupsId
)
{ {
var now = SystemClock.Instance.GetCurrentInstant();
// First try exact match (highest priority) // First try exact match (highest priority)
var exactMatch = await db.PermissionNodes var exactMatch = await db.PermissionNodes
.Where(n => (n.GroupId == null && n.Actor == actor) || .Where(n => (n.GroupId == null && n.Actor == actor && n.Type == type) ||
(n.GroupId != null && groupsId.Contains(n.GroupId.Value))) (n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
.Where(n => n.Key == key && n.Area == area) .Where(n => n.Key == key)
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (exactMatch != null) if (exactMatch != null)
{
return exactMatch; return exactMatch;
}
// If no exact match and wildcards are enabled, try wildcard matches // If no exact match and wildcards are enabled, try wildcard matches
if (!_options.EnableWildcardMatching) if (!_options.EnableWildcardMatching)
{
return null; return null;
}
var wildcardMatches = await db.PermissionNodes var wildcardMatches = await db.PermissionNodes
.Where(n => (n.GroupId == null && n.Actor == actor) || .Where(n => (n.GroupId == null && n.Actor == actor && n.Type == type) ||
(n.GroupId != null && groupsId.Contains(n.GroupId.Value))) (n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
.Where(n => (n.Key.Contains("*") || n.Area.Contains("*"))) .Where(n => EF.Functions.Like(n.Key, "%*%"))
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.Take(_options.MaxWildcardMatches) .Take(_options.MaxWildcardMatches)
@@ -147,36 +155,21 @@ public class PermissionService(
foreach (var node in wildcardMatches) foreach (var node in wildcardMatches)
{ {
var score = CalculateWildcardMatchScore(node.Area, node.Key, area, key); var score = CalculateWildcardMatchScore(node.Key, key);
if (score > bestMatchScore) if (score <= bestMatchScore) continue;
{ bestMatch = node;
bestMatch = node; bestMatchScore = score;
bestMatchScore = score;
}
} }
if (bestMatch != null) if (bestMatch != null)
{ logger.LogDebug("Found wildcard permission match: {NodeKey} for {Key}", bestMatch.Key, key);
logger.LogDebug("Found wildcard permission match: {NodeArea}:{NodeKey} for {Area}:{Key}",
bestMatch.Area, bestMatch.Key, area, key);
}
return bestMatch; return bestMatch;
} }
private static int CalculateWildcardMatchScore(string nodeArea, string nodeKey, string targetArea, string targetKey) private static int CalculateWildcardMatchScore(string nodeKey, string targetKey)
{ {
// Calculate how well the wildcard pattern matches return CalculatePatternMatchScore(nodeKey, targetKey);
// Higher score = better match
var areaScore = CalculatePatternMatchScore(nodeArea, targetArea);
var keyScore = CalculatePatternMatchScore(nodeKey, targetKey);
// Perfect match gets highest score
if (areaScore == int.MaxValue && keyScore == int.MaxValue)
return int.MaxValue;
// Prefer area matches over key matches, more specific patterns over general ones
return (areaScore * 1000) + keyScore;
} }
private static int CalculatePatternMatchScore(string pattern, string target) private static int CalculatePatternMatchScore(string pattern, string target)
@@ -184,31 +177,30 @@ public class PermissionService(
if (pattern == target) if (pattern == target)
return int.MaxValue; // Exact match return int.MaxValue; // Exact match
if (!pattern.Contains("*")) if (!pattern.Contains('*'))
return -1; // No wildcard, not a match return -1; // No wildcard, not a match
// Simple wildcard matching: * matches any sequence of characters // Simple wildcard matching: * matches any sequence of characters
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*") + "$"; var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*") + "$";
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); var regex = new System.Text.RegularExpressions.Regex(regexPattern,
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (regex.IsMatch(target)) if (!regex.IsMatch(target)) return -1; // No match
{
// Score based on specificity (shorter patterns are less specific)
var wildcardCount = pattern.Count(c => c == '*');
var length = pattern.Length;
return Math.Max(1, 1000 - (wildcardCount * 100) - length);
}
return -1; // No match // Score based on specificity (shorter patterns are less specific)
var wildcardCount = pattern.Count(c => c == '*');
var length = pattern.Length;
return Math.Max(1, 1000 - wildcardCount * 100 - length);
} }
public async Task<SnPermissionNode> AddPermissionNode<T>( public async Task<SnPermissionNode> AddPermissionNode<T>(
string actor, string actor,
string area,
string key, string key,
T value, T value,
Instant? expiredAt = null, Instant? expiredAt = null,
Instant? affectedAt = null Instant? affectedAt = null,
PermissionNodeActorType type = PermissionNodeActorType.Account
) )
{ {
if (value is null) throw new ArgumentNullException(nameof(value)); if (value is null) throw new ArgumentNullException(nameof(value));
@@ -216,8 +208,8 @@ public class PermissionService(
var node = new SnPermissionNode var node = new SnPermissionNode
{ {
Actor = actor, Actor = actor,
Type = type,
Key = key, Key = key,
Area = area,
Value = SerializePermissionValue(value), Value = SerializePermissionValue(value),
ExpiredAt = expiredAt, ExpiredAt = expiredAt,
AffectedAt = affectedAt AffectedAt = affectedAt
@@ -227,7 +219,7 @@ public class PermissionService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Invalidate related caches // Invalidate related caches
await InvalidatePermissionCacheAsync(actor, area, key); await InvalidatePermissionCacheAsync(actor, key);
return node; return node;
} }
@@ -235,11 +227,11 @@ public class PermissionService(
public async Task<SnPermissionNode> AddPermissionNodeToGroup<T>( public async Task<SnPermissionNode> AddPermissionNodeToGroup<T>(
SnPermissionGroup group, SnPermissionGroup group,
string actor, string actor,
string area,
string key, string key,
T value, T value,
Instant? expiredAt = null, Instant? expiredAt = null,
Instant? affectedAt = null Instant? affectedAt = null,
PermissionNodeActorType type = PermissionNodeActorType.Account
) )
{ {
if (value is null) throw new ArgumentNullException(nameof(value)); if (value is null) throw new ArgumentNullException(nameof(value));
@@ -247,8 +239,8 @@ public class PermissionService(
var node = new SnPermissionNode var node = new SnPermissionNode
{ {
Actor = actor, Actor = actor,
Type = type,
Key = key, Key = key,
Area = area,
Value = SerializePermissionValue(value), Value = SerializePermissionValue(value),
ExpiredAt = expiredAt, ExpiredAt = expiredAt,
AffectedAt = affectedAt, AffectedAt = affectedAt,
@@ -260,44 +252,45 @@ public class PermissionService(
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Invalidate related caches // Invalidate related caches
await InvalidatePermissionCacheAsync(actor, area, key); await InvalidatePermissionCacheAsync(actor, key);
await cache.RemoveAsync(GetGroupsCacheKey(actor)); await cache.RemoveAsync(GetGroupsCacheKey(actor));
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor)); await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
return node; return node;
} }
public async Task RemovePermissionNode(string actor, string area, string key) public async Task RemovePermissionNode(string actor, string key, PermissionNodeActorType? type)
{ {
var node = await db.PermissionNodes var node = await db.PermissionNodes
.Where(n => n.Actor == actor && n.Area == area && n.Key == key) .Where(n => n.Actor == actor && n.Key == key)
.If(type is not null, q => q.Where(n => n.Type == type))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (node is not null) db.PermissionNodes.Remove(node); if (node is not null) db.PermissionNodes.Remove(node);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Invalidate cache // Invalidate cache
await InvalidatePermissionCacheAsync(actor, area, key); await InvalidatePermissionCacheAsync(actor, key);
} }
public async Task RemovePermissionNodeFromGroup<T>(SnPermissionGroup group, string actor, string area, string key) public async Task RemovePermissionNodeFromGroup<T>(SnPermissionGroup group, string actor, string key)
{ {
var node = await db.PermissionNodes var node = await db.PermissionNodes
.Where(n => n.GroupId == group.Id) .Where(n => n.GroupId == group.Id)
.Where(n => n.Actor == actor && n.Area == area && n.Key == key) .Where(n => n.Actor == actor && n.Key == key && n.Type == PermissionNodeActorType.Group)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (node is null) return; if (node is null) return;
db.PermissionNodes.Remove(node); db.PermissionNodes.Remove(node);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
// Invalidate caches // Invalidate caches
await InvalidatePermissionCacheAsync(actor, area, key); await InvalidatePermissionCacheAsync(actor, key);
await cache.RemoveAsync(GetGroupsCacheKey(actor)); await cache.RemoveAsync(GetGroupsCacheKey(actor));
await cache.RemoveGroupAsync(GetPermissionGroupKey(actor)); await cache.RemoveGroupAsync(GetPermissionGroupKey(actor));
} }
private async Task InvalidatePermissionCacheAsync(string actor, string area, string key) private async Task InvalidatePermissionCacheAsync(string actor, string key)
{ {
var cacheKey = GetPermissionCacheKey(actor, area, key); var cacheKey = GetPermissionCacheKey(actor, key);
await cache.RemoveAsync(cacheKey); await cache.RemoveAsync(cacheKey);
} }
@@ -312,12 +305,11 @@ public class PermissionService(
return JsonDocument.Parse(str); return JsonDocument.Parse(str);
} }
public static SnPermissionNode NewPermissionNode<T>(string actor, string area, string key, T value) public static SnPermissionNode NewPermissionNode<T>(string actor, string key, T value)
{ {
return new SnPermissionNode return new SnPermissionNode
{ {
Actor = actor, Actor = actor,
Area = area,
Key = key, Key = key,
Value = SerializePermissionValue(value), Value = SerializePermissionValue(value),
}; };
@@ -341,8 +333,7 @@ public class PermissionService(
(n.GroupId != null && groupsId.Contains(n.GroupId.Value))) (n.GroupId != null && groupsId.Contains(n.GroupId.Value)))
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.OrderBy(n => n.Area) .OrderBy(n => n.Key)
.ThenBy(n => n.Key)
.ToListAsync(); .ToListAsync();
logger.LogDebug("Listed {Count} effective permissions for actor {Actor}", permissions.Count, actor); logger.LogDebug("Listed {Count} effective permissions for actor {Actor}", permissions.Count, actor);
@@ -370,8 +361,7 @@ public class PermissionService(
.Where(n => n.GroupId == null && n.Actor == actor) .Where(n => n.GroupId == null && n.Actor == actor)
.Where(n => n.ExpiredAt == null || n.ExpiredAt > now) .Where(n => n.ExpiredAt == null || n.ExpiredAt > now)
.Where(n => n.AffectedAt == null || n.AffectedAt <= now) .Where(n => n.AffectedAt == null || n.AffectedAt <= now)
.OrderBy(n => n.Area) .OrderBy(n => n.Key)
.ThenBy(n => n.Key)
.ToListAsync(); .ToListAsync();
logger.LogDebug("Listed {Count} direct permissions for actor {Actor}", permissions.Count, actor); logger.LogDebug("Listed {Count} direct permissions for actor {Actor}", permissions.Count, actor);
@@ -424,4 +414,4 @@ public class PermissionService(
throw; throw;
} }
} }
} }

View File

@@ -9,31 +9,33 @@ using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Pass.Permission; namespace DysonNetwork.Pass.Permission;
public class PermissionServiceGrpc( public class PermissionServiceGrpc(
PermissionService permissionService, PermissionService psv,
AppDatabase db, AppDatabase db,
ILogger<PermissionServiceGrpc> logger ILogger<PermissionServiceGrpc> logger
) : DysonNetwork.Shared.Proto.PermissionService.PermissionServiceBase ) : DysonNetwork.Shared.Proto.PermissionService.PermissionServiceBase
{ {
public override async Task<HasPermissionResponse> HasPermission(HasPermissionRequest request, ServerCallContext context) public override async Task<HasPermissionResponse> HasPermission(HasPermissionRequest request, ServerCallContext context)
{ {
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try try
{ {
var hasPermission = await permissionService.HasPermissionAsync(request.Actor, request.Area, request.Key); var hasPermission = await psv.HasPermissionAsync(request.Actor, request.Key, type);
return new HasPermissionResponse { HasPermission = hasPermission }; return new HasPermissionResponse { HasPermission = hasPermission };
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error checking permission for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Error checking permission for {Type}:{Area}:{Key}",
request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Permission check failed")); throw new RpcException(new Status(StatusCode.Internal, "Permission check failed"));
} }
} }
public override async Task<GetPermissionResponse> GetPermission(GetPermissionRequest request, ServerCallContext context) public override async Task<GetPermissionResponse> GetPermission(GetPermissionRequest request, ServerCallContext context)
{ {
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try try
{ {
var permissionValue = await permissionService.GetPermissionAsync<JsonDocument>(request.Actor, request.Area, request.Key); var permissionValue = await psv.GetPermissionAsync<JsonDocument>(request.Actor, request.Key, type);
return new GetPermissionResponse return new GetPermissionResponse
{ {
Value = permissionValue != null ? Value.Parser.ParseJson(permissionValue.RootElement.GetRawText()) : null Value = permissionValue != null ? Value.Parser.ParseJson(permissionValue.RootElement.GetRawText()) : null
@@ -41,14 +43,15 @@ public class PermissionServiceGrpc(
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error getting permission for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Error getting permission for {Type}:{Area}:{Key}",
request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to retrieve permission")); throw new RpcException(new Status(StatusCode.Internal, "Failed to retrieve permission"));
} }
} }
public override async Task<AddPermissionNodeResponse> AddPermissionNode(AddPermissionNodeRequest request, ServerCallContext context) public override async Task<AddPermissionNodeResponse> AddPermissionNode(AddPermissionNodeRequest request, ServerCallContext context)
{ {
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try try
{ {
JsonDocument jsonValue; JsonDocument jsonValue;
@@ -58,18 +61,18 @@ public class PermissionServiceGrpc(
} }
catch (JsonException ex) catch (JsonException ex)
{ {
logger.LogWarning(ex, "Invalid JSON in permission value for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}",
request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
} }
var node = await permissionService.AddPermissionNode( var node = await psv.AddPermissionNode(
request.Actor, request.Actor,
request.Area,
request.Key, request.Key,
jsonValue, jsonValue,
request.ExpiredAt?.ToInstant(), request.ExpiredAt?.ToInstant(),
request.AffectedAt?.ToInstant() request.AffectedAt?.ToInstant(),
type
); );
return new AddPermissionNodeResponse { Node = node.ToProtoValue() }; return new AddPermissionNodeResponse { Node = node.ToProtoValue() };
} }
@@ -79,14 +82,15 @@ public class PermissionServiceGrpc(
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error adding permission node for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}",
request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node")); throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node"));
} }
} }
public override async Task<AddPermissionNodeToGroupResponse> AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest request, ServerCallContext context) public override async Task<AddPermissionNodeToGroupResponse> AddPermissionNodeToGroup(AddPermissionNodeToGroupRequest request, ServerCallContext context)
{ {
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try try
{ {
var group = await FindPermissionGroupAsync(request.Group.Id); var group = await FindPermissionGroupAsync(request.Group.Id);
@@ -102,19 +106,19 @@ public class PermissionServiceGrpc(
} }
catch (JsonException ex) catch (JsonException ex)
{ {
logger.LogWarning(ex, "Invalid JSON in permission value for group {GroupId}, actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Invalid JSON in permission value for {Type}:{Area}:{Key}",
request.Group.Id, request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid permission value format"));
} }
var node = await permissionService.AddPermissionNodeToGroup( var node = await psv.AddPermissionNodeToGroup(
group, group,
request.Actor, request.Actor,
request.Area,
request.Key, request.Key,
jsonValue, jsonValue,
request.ExpiredAt?.ToInstant(), request.ExpiredAt?.ToInstant(),
request.AffectedAt?.ToInstant() request.AffectedAt?.ToInstant(),
type
); );
return new AddPermissionNodeToGroupResponse { Node = node.ToProtoValue() }; return new AddPermissionNodeToGroupResponse { Node = node.ToProtoValue() };
} }
@@ -124,23 +128,24 @@ public class PermissionServiceGrpc(
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error adding permission node to group {GroupId} for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Error adding permission for {Type}:{Area}:{Key}",
request.Group.Id, request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node to group")); throw new RpcException(new Status(StatusCode.Internal, "Failed to add permission node to group"));
} }
} }
public override async Task<RemovePermissionNodeResponse> RemovePermissionNode(RemovePermissionNodeRequest request, ServerCallContext context) public override async Task<RemovePermissionNodeResponse> RemovePermissionNode(RemovePermissionNodeRequest request, ServerCallContext context)
{ {
var type = SnPermissionNode.ConvertProtoActorType(request.Type);
try try
{ {
await permissionService.RemovePermissionNode(request.Actor, request.Area, request.Key); await psv.RemovePermissionNode(request.Actor, request.Key, type);
return new RemovePermissionNodeResponse { Success = true }; return new RemovePermissionNodeResponse { Success = true };
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error removing permission node for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Error removing permission for {Type}:{Area}:{Key}",
request.Actor, request.Area, request.Key); type, request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node")); throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node"));
} }
} }
@@ -155,7 +160,7 @@ public class PermissionServiceGrpc(
throw new RpcException(new Status(StatusCode.NotFound, "Permission group not found")); throw new RpcException(new Status(StatusCode.NotFound, "Permission group not found"));
} }
await permissionService.RemovePermissionNodeFromGroup<JsonDocument>(group, request.Actor, request.Area, request.Key); await psv.RemovePermissionNodeFromGroup<JsonDocument>(group, request.Actor, request.Key);
return new RemovePermissionNodeFromGroupResponse { Success = true }; return new RemovePermissionNodeFromGroupResponse { Success = true };
} }
catch (RpcException) catch (RpcException)
@@ -164,20 +169,18 @@ public class PermissionServiceGrpc(
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Error removing permission node from group {GroupId} for actor {Actor}, area {Area}, key {Key}", logger.LogError(ex, "Error removing permission from group for {Area}:{Key}",
request.Group.Id, request.Actor, request.Area, request.Key); request.Actor, request.Key);
throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node from group")); throw new RpcException(new Status(StatusCode.Internal, "Failed to remove permission node from group"));
} }
} }
private async Task<SnPermissionGroup?> FindPermissionGroupAsync(string groupId) private async Task<SnPermissionGroup?> FindPermissionGroupAsync(string groupId)
{ {
if (!Guid.TryParse(groupId, out var guid)) if (Guid.TryParse(groupId, out var guid))
{ return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid);
logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId); logger.LogWarning("Invalid GUID format for group ID: {GroupId}", groupId);
return null; return null;
}
return await db.PermissionGroups.FirstOrDefaultAsync(g => g.Id == guid);
} }
} }

View File

@@ -5,6 +5,7 @@ using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using NodaTime; using NodaTime;
using System.Text.Json; using System.Text.Json;
using DysonNetwork.Shared.Auth;
namespace DysonNetwork.Pass; namespace DysonNetwork.Pass;
@@ -19,16 +20,20 @@ public class PermissionController(
/// <summary> /// <summary>
/// Check if an actor has a specific permission /// Check if an actor has a specific permission
/// </summary> /// </summary>
[HttpGet("check/{actor}/{area}/{key}")] [HttpGet("check/{actor}/{key}")]
[RequiredPermission("maintenance", "permissions.check")] [AskPermission("permissions.check")]
[ProducesResponseType<bool>(StatusCodes.Status200OK)] [ProducesResponseType<bool>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> CheckPermission(string actor, string area, string key) public async Task<IActionResult> CheckPermission(
[FromRoute] string actor,
[FromRoute] string key,
[FromQuery] PermissionNodeActorType type = PermissionNodeActorType.Account
)
{ {
try try
{ {
var hasPermission = await permissionService.HasPermissionAsync(actor, area, key); var hasPermission = await permissionService.HasPermissionAsync(actor, key, type);
return Ok(hasPermission); return Ok(hasPermission);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
@@ -45,7 +50,7 @@ public class PermissionController(
/// Get all effective permissions for an actor (including group permissions) /// Get all effective permissions for an actor (including group permissions)
/// </summary> /// </summary>
[HttpGet("actors/{actor}/permissions/effective")] [HttpGet("actors/{actor}/permissions/effective")]
[RequiredPermission("maintenance", "permissions.check")] [AskPermission("permissions.check")]
[ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)] [ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -70,7 +75,7 @@ public class PermissionController(
/// Get all direct permissions for an actor (excluding group permissions) /// Get all direct permissions for an actor (excluding group permissions)
/// </summary> /// </summary>
[HttpGet("actors/{actor}/permissions/direct")] [HttpGet("actors/{actor}/permissions/direct")]
[RequiredPermission("maintenance", "permissions.check")] [AskPermission("permissions.check")]
[ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)] [ProducesResponseType<List<SnPermissionNode>>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -94,28 +99,27 @@ public class PermissionController(
/// <summary> /// <summary>
/// Give a permission to an actor /// Give a permission to an actor
/// </summary> /// </summary>
[HttpPost("actors/{actor}/permissions/{area}/{key}")] [HttpPost("actors/{actor}/permissions/{key}")]
[RequiredPermission("maintenance", "permissions.manage")] [AskPermission("permissions.manage")]
[ProducesResponseType<SnPermissionNode>(StatusCodes.Status201Created)] [ProducesResponseType<SnPermissionNode>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GivePermission( public async Task<IActionResult> GivePermission(
string actor, string actor,
string area,
string key, string key,
[FromBody] PermissionRequest request) [FromBody] PermissionRequest request
)
{ {
try try
{ {
var permission = await permissionService.AddPermissionNode( var permission = await permissionService.AddPermissionNode(
actor, actor,
area,
key, key,
JsonDocument.Parse(JsonSerializer.Serialize(request.Value)), JsonDocument.Parse(JsonSerializer.Serialize(request.Value)),
request.ExpiredAt, request.ExpiredAt,
request.AffectedAt request.AffectedAt
); );
return Created($"/api/permissions/actors/{actor}/permissions/{area}/{key}", permission); return Created($"/api/permissions/actors/{actor}/permissions/{key}", permission);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
@@ -130,16 +134,20 @@ public class PermissionController(
/// <summary> /// <summary>
/// Remove a permission from an actor /// Remove a permission from an actor
/// </summary> /// </summary>
[HttpDelete("actors/{actor}/permissions/{area}/{key}")] [HttpDelete("actors/{actor}/permissions/{key}")]
[RequiredPermission("maintenance", "permissions.manage")] [AskPermission("permissions.manage")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> RemovePermission(string actor, string area, string key) public async Task<IActionResult> RemovePermission(
string actor,
string key,
[FromQuery] PermissionNodeActorType type = PermissionNodeActorType.Account
)
{ {
try try
{ {
await permissionService.RemovePermissionNode(actor, area, key); await permissionService.RemovePermissionNode(actor, key, type);
return NoContent(); return NoContent();
} }
catch (ArgumentException ex) catch (ArgumentException ex)
@@ -156,7 +164,7 @@ public class PermissionController(
/// Get all groups for an actor /// Get all groups for an actor
/// </summary> /// </summary>
[HttpGet("actors/{actor}/groups")] [HttpGet("actors/{actor}/groups")]
[RequiredPermission("maintenance", "permissions.groups.check")] [AskPermission("permissions.groups.check")]
[ProducesResponseType<List<SnPermissionGroupMember>>(StatusCodes.Status200OK)] [ProducesResponseType<List<SnPermissionGroupMember>>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -183,8 +191,8 @@ public class PermissionController(
/// <summary> /// <summary>
/// Add an actor to a permission group /// Add an actor to a permission group
/// </summary> /// </summary>
[HttpPost("actors/{actor}/groups/{groupId}")] [HttpPost("actors/{actor}/groups/{groupId:guid}")]
[RequiredPermission("maintenance", "permissions.groups.manage")] [AskPermission("permissions.groups.manage")]
[ProducesResponseType<SnPermissionGroupMember>(StatusCodes.Status201Created)] [ProducesResponseType<SnPermissionGroupMember>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -192,7 +200,8 @@ public class PermissionController(
public async Task<IActionResult> AddActorToGroup( public async Task<IActionResult> AddActorToGroup(
string actor, string actor,
Guid groupId, Guid groupId,
[FromBody] GroupMembershipRequest? request = null) [FromBody] GroupMembershipRequest? request = null
)
{ {
try try
{ {
@@ -238,7 +247,7 @@ public class PermissionController(
/// Remove an actor from a permission group /// Remove an actor from a permission group
/// </summary> /// </summary>
[HttpDelete("actors/{actor}/groups/{groupId}")] [HttpDelete("actors/{actor}/groups/{groupId}")]
[RequiredPermission("maintenance", "permissions.groups.manage")] [AskPermission("permissions.groups.manage")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -272,7 +281,7 @@ public class PermissionController(
/// Clear permission cache for an actor /// Clear permission cache for an actor
/// </summary> /// </summary>
[HttpPost("actors/{actor}/cache/clear")] [HttpPost("actors/{actor}/cache/clear")]
[RequiredPermission("maintenance", "permissions.cache.manage")] [AskPermission("permissions.cache.manage")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -297,7 +306,7 @@ public class PermissionController(
/// Validate a permission pattern /// Validate a permission pattern
/// </summary> /// </summary>
[HttpPost("validate-pattern")] [HttpPost("validate-pattern")]
[RequiredPermission("maintenance", "permissions.check")] [AskPermission("permissions.check")]
[ProducesResponseType<PatternValidationResponse>(StatusCodes.Status200OK)] [ProducesResponseType<PatternValidationResponse>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult ValidatePattern([FromBody] PatternValidationRequest request) public IActionResult ValidatePattern([FromBody] PatternValidationRequest request)
@@ -322,14 +331,14 @@ public class PermissionController(
public class PermissionRequest public class PermissionRequest
{ {
public object? Value { get; set; } public object? Value { get; set; }
public NodaTime.Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }
public NodaTime.Instant? AffectedAt { get; set; } public Instant? AffectedAt { get; set; }
} }
public class GroupMembershipRequest public class GroupMembershipRequest
{ {
public NodaTime.Instant? ExpiredAt { get; set; } public Instant? ExpiredAt { get; set; }
public NodaTime.Instant? AffectedAt { get; set; } public Instant? AffectedAt { get; set; }
} }
public class PatternValidationRequest public class PatternValidationRequest
@@ -342,4 +351,4 @@ public class PatternValidationResponse
public string Pattern { get; set; } = string.Empty; public string Pattern { get; set; } = string.Empty;
public bool IsValid { get; set; } public bool IsValid { get; set; }
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
} }

View File

@@ -35,7 +35,8 @@ public class RealmServiceGrpc(
: realm.ToProtoValue(); : realm.ToProtoValue();
} }
public override async Task<GetRealmBatchResponse> GetRealmBatch(GetRealmBatchRequest request, ServerCallContext context) public override async Task<GetRealmBatchResponse> GetRealmBatch(GetRealmBatchRequest request,
ServerCallContext context)
{ {
var ids = request.Ids.Select(Guid.Parse).ToList(); var ids = request.Ids.Select(Guid.Parse).ToList();
var realms = await db.Realms.Where(r => ids.Contains(r.Id)).ToListAsync(); var realms = await db.Realms.Where(r => ids.Contains(r.Id)).ToListAsync();
@@ -67,19 +68,33 @@ public class RealmServiceGrpc(
return new GetUserRealmsResponse { RealmIds = { realms.Select(g => g.ToString()) } }; return new GetUserRealmsResponse { RealmIds = { realms.Select(g => g.ToString()) } };
} }
public override async Task<GetPublicRealmsResponse> GetPublicRealms(Empty request, ServerCallContext context) public override Task<GetPublicRealmsResponse> GetPublicRealms(
GetPublicRealmsRequest request,
ServerCallContext context
)
{ {
var realms = await db.Realms.Where(r => r.IsPublic).ToListAsync(); var realmsQueryable = db.Realms.Where(r => r.IsPublic).AsQueryable();
realmsQueryable = request.OrderBy switch
{
"random" => realmsQueryable.OrderBy(_ => EF.Functions.Random()),
"name" => realmsQueryable.OrderBy(r => r.Name),
"popularity" => realmsQueryable.OrderByDescending(r => r.Members.Count),
_ => realmsQueryable.OrderByDescending(r => r.CreatedAt)
};
var response = new GetPublicRealmsResponse(); var response = new GetPublicRealmsResponse();
response.Realms.AddRange(realms.Select(r => r.ToProtoValue())); response.Realms.AddRange(realmsQueryable.Select(r => r.ToProtoValue()));
return response; return Task.FromResult(response);
} }
public override async Task<GetPublicRealmsResponse> SearchRealms(SearchRealmsRequest request, ServerCallContext context) public override async Task<GetPublicRealmsResponse> SearchRealms(SearchRealmsRequest request,
ServerCallContext context)
{ {
var realms = await db.Realms var realms = await db.Realms
.Where(r => r.IsPublic) .Where(r => r.IsPublic)
.Where(r => EF.Functions.Like(r.Slug, $"{request.Query}%") || EF.Functions.Like(r.Name, $"{request.Query}%")) .Where(r => EF.Functions.Like(r.Slug, $"{request.Query}%") ||
EF.Functions.Like(r.Name, $"{request.Query}%"))
.Take(request.Limit) .Take(request.Limit)
.ToListAsync(); .ToListAsync();
var response = new GetPublicRealmsResponse(); var response = new GetPublicRealmsResponse();
@@ -94,9 +109,9 @@ public class RealmServiceGrpc(
.AsNoTracking() .AsNoTracking()
.Include(a => a.Profile) .Include(a => a.Profile)
.FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId)); .FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId));
if (account == null) throw new RpcException(new Status(StatusCode.NotFound, "Account not found")); if (account == null) throw new RpcException(new Status(StatusCode.NotFound, "Account not found"));
CultureService.SetCultureInfo(account.Language); CultureService.SetCultureInfo(account.Language);
await pusher.SendPushNotificationToUserAsync( await pusher.SendPushNotificationToUserAsync(
@@ -138,7 +153,7 @@ public class RealmServiceGrpc(
.AsNoTracking() .AsNoTracking()
.Include(a => a.Profile) .Include(a => a.Profile)
.FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId)); .FirstOrDefaultAsync(a => a.Id == Guid.Parse(member.AccountId));
var response = new RealmMember(member) { Account = account?.ToProtoValue() }; var response = new RealmMember(member) { Account = account?.ToProtoValue() };
return response; return response;
} }
@@ -167,4 +182,4 @@ public class RealmServiceGrpc(
return response; return response;
} }
} }

View File

@@ -57,18 +57,6 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_1 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_1", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_1 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_1", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_2 { internal static string FortuneTipPositiveTitle_2 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveTitle_2", resourceCulture); return ResourceManager.GetString("FortuneTipPositiveTitle_2", resourceCulture);
@@ -81,18 +69,6 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_2 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_2", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_2 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_2", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_3 { internal static string FortuneTipPositiveTitle_3 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveTitle_3", resourceCulture); return ResourceManager.GetString("FortuneTipPositiveTitle_3", resourceCulture);
@@ -105,18 +81,6 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_3 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_3", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_3 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_3", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_4 { internal static string FortuneTipPositiveTitle_4 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveTitle_4", resourceCulture); return ResourceManager.GetString("FortuneTipPositiveTitle_4", resourceCulture);
@@ -129,18 +93,6 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_4 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_4", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_4 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_4", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_5 { internal static string FortuneTipPositiveTitle_5 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveTitle_5", resourceCulture); return ResourceManager.GetString("FortuneTipPositiveTitle_5", resourceCulture);
@@ -153,18 +105,6 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_5 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_5", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_5 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_5", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_6 { internal static string FortuneTipPositiveTitle_6 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveTitle_6", resourceCulture); return ResourceManager.GetString("FortuneTipPositiveTitle_6", resourceCulture);
@@ -177,18 +117,6 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_6 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_6", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_6 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_6", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_7 { internal static string FortuneTipPositiveTitle_7 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveTitle_7", resourceCulture); return ResourceManager.GetString("FortuneTipPositiveTitle_7", resourceCulture);
@@ -201,6 +129,162 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipPositiveTitle_8 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_8", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_8 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_8", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_9 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_9", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_9 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_9", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_10 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_10", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_10 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_10", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_11 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_11", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_11 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_11", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_12 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_12", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_12 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_12", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_13 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_13", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_13 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_13", resourceCulture);
}
}
internal static string FortuneTipPositiveTitle_14 {
get {
return ResourceManager.GetString("FortuneTipPositiveTitle_14", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_14 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_14", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_1 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_1", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_1 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_1", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_2 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_2", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_2 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_2", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_3 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_3", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_3 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_3", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_4 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_4", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_4 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_4", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_5 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_5", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_5 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_5", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_6 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_6", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_6 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_6", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_7 { internal static string FortuneTipNegativeTitle_7 {
get { get {
return ResourceManager.GetString("FortuneTipNegativeTitle_7", resourceCulture); return ResourceManager.GetString("FortuneTipNegativeTitle_7", resourceCulture);
@@ -213,15 +297,117 @@ namespace DysonNetwork.Sphere.Resources {
} }
} }
internal static string FortuneTipNegativeTitle_1_ { internal static string FortuneTipNegativeTitle_8 {
get { get {
return ResourceManager.GetString("FortuneTipNegativeTitle_1 ", resourceCulture); return ResourceManager.GetString("FortuneTipNegativeTitle_8", resourceCulture);
} }
} }
internal static string FortuneTipPositiveContent_14 { internal static string FortuneTipNegativeContent_8 {
get { get {
return ResourceManager.GetString("FortuneTipPositiveContent_14", resourceCulture); return ResourceManager.GetString("FortuneTipNegativeContent_8", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_9 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_9", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_9 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_9", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_10 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_10", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_10 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_10", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_11 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_11", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_11 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_11", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_12 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_12", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_12 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_12", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_13 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_13", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_13 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_13", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_14 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_14", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_14 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_14", resourceCulture);
}
}
internal static string FortuneTipNegativeTitle_15 {
get {
return ResourceManager.GetString("FortuneTipNegativeTitle_15", resourceCulture);
}
}
internal static string FortuneTipPositiveContent_15 {
get {
return ResourceManager.GetString("FortuneTipPositiveContent_15", resourceCulture);
}
}
internal static string FortuneTipNegativeContent_15 {
get {
return ResourceManager.GetString("FortuneTipNegativeContent_15", resourceCulture);
}
}
internal static string FortuneTipSpecialTitle_Birthday {
get {
return ResourceManager.GetString("FortuneTipSpecialTitle_Birthday", resourceCulture);
}
}
internal static string FortuneTipSpecialContent_Birthday {
get {
return ResourceManager.GetString("FortuneTipSpecialContent_Birthday", resourceCulture);
} }
} }
} }

View File

@@ -195,4 +195,10 @@
<value>“Why is there still something in the box“</value> <value>“Why is there still something in the box“</value>
<comment/> <comment/>
</data> </data>
<data name="FortuneTipSpecialTitle_Birthday" xml:space="preserve">
<value>Have a Birthday Party</value>
</data>
<data name="FortuneTipSpecialContent_Birthday" xml:space="preserve">
<value>Happy Birthday, {0}!</value>
</data>
</root> </root>

View File

@@ -248,4 +248,10 @@
<value>“?暗盒里怎么还有!“</value> <value>“?暗盒里怎么还有!“</value>
<comment/> <comment/>
</data> </data>
<data name="FortuneTipSpecialTitle_Birthday" xml:space="preserve">
<value>过生日</value>
</data>
<data name="FortuneTipSpecialContent_Birthday" xml:space="preserve">
<value>生日快乐,{0}</value>
</data>
</root> </root>

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -51,7 +52,7 @@ public class SnAbuseReportController(
[HttpGet("")] [HttpGet("")]
[Authorize] [Authorize]
[RequiredPermission("safety", "reports.view")] [AskPermission("reports.view")]
[ProducesResponseType<List<SnAbuseReport>>(StatusCodes.Status200OK)] [ProducesResponseType<List<SnAbuseReport>>(StatusCodes.Status200OK)]
public async Task<ActionResult<List<SnAbuseReport>>> GetReports( public async Task<ActionResult<List<SnAbuseReport>>> GetReports(
[FromQuery] int offset = 0, [FromQuery] int offset = 0,
@@ -85,7 +86,7 @@ public class SnAbuseReportController(
[HttpGet("{id}")] [HttpGet("{id}")]
[Authorize] [Authorize]
[RequiredPermission("safety", "reports.view")] [AskPermission("reports.view")]
[ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)] [ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<SnAbuseReport>> GetReportById(Guid id) public async Task<ActionResult<SnAbuseReport>> GetReportById(Guid id)
@@ -122,7 +123,7 @@ public class SnAbuseReportController(
[HttpPost("{id}/resolve")] [HttpPost("{id}/resolve")]
[Authorize] [Authorize]
[RequiredPermission("safety", "reports.resolve")] [AskPermission("reports.resolve")]
[ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)] [ProducesResponseType<SnAbuseReport>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<SnAbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request) public async Task<ActionResult<SnAbuseReport>> ResolveReport(Guid id, [FromBody] ResolveReportRequest request)
@@ -144,7 +145,7 @@ public class SnAbuseReportController(
[HttpGet("count")] [HttpGet("count")]
[Authorize] [Authorize]
[RequiredPermission("safety", "reports.view")] [AskPermission("reports.view")]
[ProducesResponseType<object>(StatusCodes.Status200OK)] [ProducesResponseType<object>(StatusCodes.Status200OK)]
public async Task<ActionResult<object>> GetReportsCount() public async Task<ActionResult<object>> GetReportsCount()
{ {

View File

@@ -22,7 +22,7 @@ public static class ApplicationConfiguration
app.UseWebSockets(); app.UseWebSockets();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseMiddleware<PermissionMiddleware>(); app.UseMiddleware<LocalPermissionMiddleware>();
app.MapControllers().RequireRateLimiting("fixed"); app.MapControllers().RequireRateLimiting("fixed");

Some files were not shown because too many files have changed in this diff Show More