Compare commits

...

74 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
197 changed files with 12143 additions and 35530 deletions

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

@@ -12,10 +12,16 @@
"ConnectionStrings": { "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" "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"], "KnownProxies": [
"127.0.0.1",
"::1"
],
"Swagger": { "Swagger": {
"PublicBasePath": "/develop" "PublicBasePath": "/develop"
}, },
"Cache": {
"Serializer": "MessagePack"
},
"Etcd": { "Etcd": {
"Insecure": true "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;

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

@@ -381,7 +381,7 @@ 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();

View File

@@ -58,12 +58,15 @@ public class FileReferenceService(AppDatabase db, FileService fileService, ICach
Duration? duration = null Duration? duration = null
) )
{ {
var now = SystemClock.Instance.GetCurrentInstant();
var data = fileId.Select(id => new SnCloudFileReference 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;

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

@@ -111,6 +111,9 @@
} }
} }
}, },
"Cache": {
"Serializer": "MessagePack"
},
"KnownProxies": [ "KnownProxies": [
"127.0.0.1", "127.0.0.1",
"::1" "::1"

View File

@@ -5,6 +5,9 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"Cache": {
"Serializer": "MessagePack"
},
"AllowedHosts": "*", "AllowedHosts": "*",
"SiteUrl": "http://localhost:3000", "SiteUrl": "http://localhost:3000",
"Client": { "Client": {

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;
@@ -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,12 +313,52 @@ public class AccountEventService(
CultureInfo.CurrentCulture = cultureInfo; CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo; CultureInfo.CurrentUICulture = cultureInfo;
var accountProfile = await db.AccountProfiles
.Where(x => x.AccountId == user.Id)
.Select(x => new { x.Birthday, x.TimeZone })
.FirstOrDefaultAsync();
var accountBirthday = accountProfile?.Birthday;
var now = SystemClock.Instance.GetCurrentInstant();
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;
tips = [
new CheckInFortuneTip()
{
IsPositive = true,
Title = localizer["FortuneTipSpecialTitle_Birthday"].Value,
Content = localizer["FortuneTipSpecialContent_Birthday", user.Nick].Value,
}
];
}
else
{
// Generate 2 positive tips // Generate 2 positive tips
var positiveIndices = Enumerable.Range(1, FortuneTipCount) var positiveIndices = Enumerable.Range(1, FortuneTipCount)
.OrderBy(_ => Random.Next()) .OrderBy(_ => Random.Next())
.Take(2) .Take(2)
.ToList(); .ToList();
var tips = positiveIndices.Select(index => new CheckInFortuneTip tips = positiveIndices.Select(index => new CheckInFortuneTip
{ {
IsPositive = true, IsPositive = true,
Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value, Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value,
@@ -342,7 +382,7 @@ public class AccountEventService(
// Use weighted random distribution to make all levels reasonably achievable // Use weighted random distribution to make all levels reasonably achievable
// Weights: Worst: 10%, Worse: 20%, Normal: 40%, Better: 20%, Best: 10% // Weights: Worst: 10%, Worse: 20%, Normal: 40%, Better: 20%, Best: 10%
var randomValue = Random.Next(100); var randomValue = Random.Next(100);
var checkInLevel = randomValue switch checkInLevel = randomValue switch
{ {
< 10 => CheckInResultLevel.Worst, // 0-9: 10% chance < 10 => CheckInResultLevel.Worst, // 0-9: 10% chance
< 30 => CheckInResultLevel.Worse, // 10-29: 20% chance < 30 => CheckInResultLevel.Worse, // 10-29: 20% chance
@@ -350,15 +390,7 @@ public class AccountEventService(
< 90 => CheckInResultLevel.Better, // 70-89: 20% chance < 90 => CheckInResultLevel.Better, // 70-89: 20% chance
_ => CheckInResultLevel.Best // 90-99: 10% chance _ => CheckInResultLevel.Best // 90-99: 10% chance
}; };
}
var accountBirthday = await db.AccountProfiles
.Where(x => x.AccountId == user.Id)
.Select(x => x.Birthday)
.FirstOrDefaultAsync();
var now = SystemClock.Instance.GetCurrentInstant().InUtc().Date;
if (accountBirthday.HasValue && accountBirthday.Value.InUtc().Date == now)
checkInLevel = CheckInResultLevel.Special;
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
@@ -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

@@ -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,6 +9,20 @@ 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)
{ {
@@ -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

@@ -3,6 +3,7 @@ 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.Data;
using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
@@ -14,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!;
@@ -31,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;
@@ -42,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();
@@ -60,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;
@@ -81,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)
{ {
@@ -157,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();
@@ -181,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();
@@ -217,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
{ {
@@ -288,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();
var accountIdsToClearCache = new HashSet<Guid>();
// Fetch all sessions to be revoked in one go
var sessions = await db.AuthSessions
.Where(s => sessionsToRevokeIds.Contains(s.Id))
.ToListAsync();
foreach (var session in sessions)
{
session.ExpiredAt = now; session.ExpiredAt = now;
db.AuthSessions.Update(session); accountIdsToClearCache.Add(session.AccountId);
// Clear from cache immediately // Clear from cache immediately for each session
var cacheKey = $"{AuthCachePrefix}{session.Id}"; await cache.RemoveAsync($"{AuthCachePrefix}{session.Id}");
await cache.RemoveAsync(cacheKey); }
// Clear account-level cache groups that include this session
await cache.RemoveAsync($"{AuthCachePrefix}{session.AccountId}");
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>
@@ -375,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
@@ -386,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);
@@ -409,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();
@@ -500,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
{ {
@@ -509,7 +561,8 @@ public class AuthService(
Session = new SnAuthSession Session = new SnAuthSession
{ {
AccountId = accountId, AccountId = accountId,
ExpiredAt = expiredAt ExpiredAt = expiredAt,
ParentSessionId = parentSession?.Id
}, },
}; };
@@ -615,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

@@ -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,84 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddCreditAndLevelingRecords : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
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),
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: "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),
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.CreateIndex(
name: "ix_experience_records_account_id",
table: "experience_records",
column: "account_id");
migrationBuilder.CreateIndex(
name: "ix_social_credit_records_account_id",
table: "social_credit_records",
column: "account_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "experience_records");
migrationBuilder.DropTable(
name: "social_credit_records");
}
}
}

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,78 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
#nullable disable
namespace DysonNetwork.Pass.Migrations
{
/// <inheritdoc />
public partial class AddLotteries : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
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),
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(
name: "lottery_records",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
draw_date = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
winning_region_one_numbers = table.Column<List<int>>(type: "jsonb", nullable: false),
winning_region_two_number = table.Column<int>(type: "integer", nullable: false),
total_tickets = table.Column<int>(type: "integer", nullable: false),
total_prizes_awarded = table.Column<int>(type: "integer", nullable: false),
total_prize_amount = table.Column<long>(type: "bigint", 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_lottery_records", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_lotteries_account_id",
table: "lotteries",
column: "account_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "lotteries");
migrationBuilder.DropTable(
name: "lottery_records");
}
}
}

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)
@@ -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;
@@ -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)) return -1; // No match
if (regex.IsMatch(target))
{
// Score based on specificity (shorter patterns are less specific) // Score based on specificity (shorter patterns are less specific)
var wildcardCount = pattern.Count(c => c == '*'); var wildcardCount = pattern.Count(c => c == '*');
var length = pattern.Length; var length = pattern.Length;
return Math.Max(1, 1000 - (wildcardCount * 100) - length);
}
return -1; // No match 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);

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

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();

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");

View File

@@ -170,12 +170,12 @@ public class BroadcastEventHandler(
await nats.PublishAsync( await nats.PublishAsync(
AccountStatusUpdatedEvent.Type, AccountStatusUpdatedEvent.Type,
GrpcTypeHelper.ConvertObjectToByteString(new AccountStatusUpdatedEvent ByteString.CopyFromUtf8(JsonSerializer.Serialize(new AccountStatusUpdatedEvent
{ {
AccountId = evt.AccountId, AccountId = evt.AccountId,
Status = status, Status = status,
UpdatedAt = SystemClock.Instance.GetCurrentInstant() UpdatedAt = SystemClock.Instance.GetCurrentInstant()
}).ToByteArray() }, GrpcTypeHelper.SerializerOptionsWithIgnore)).ToByteArray()
); );
logger.LogInformation("Broadcasted status update for user {AccountId}", evt.AccountId); logger.LogInformation("Broadcasted status update for user {AccountId}", evt.AccountId);
@@ -209,12 +209,12 @@ public class BroadcastEventHandler(
await nats.PublishAsync( await nats.PublishAsync(
AccountStatusUpdatedEvent.Type, AccountStatusUpdatedEvent.Type,
GrpcTypeHelper.ConvertObjectToByteString(new AccountStatusUpdatedEvent ByteString.CopyFromUtf8(JsonSerializer.Serialize(new AccountStatusUpdatedEvent
{ {
AccountId = evt.AccountId, AccountId = evt.AccountId,
Status = status, Status = status,
UpdatedAt = SystemClock.Instance.GetCurrentInstant() UpdatedAt = SystemClock.Instance.GetCurrentInstant()
}).ToByteArray() }, GrpcTypeHelper.SerializerOptionsWithIgnore)).ToByteArray()
); );
logger.LogInformation("Broadcasted status update for user {AccountId}", evt.AccountId); logger.LogInformation("Broadcasted status update for user {AccountId}", evt.AccountId);

View File

@@ -11,6 +11,7 @@ using NodaTime.Serialization.SystemTextJson;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Pass.Account.Presences; using DysonNetwork.Pass.Account.Presences;
using DysonNetwork.Pass.Affiliation;
using DysonNetwork.Pass.Auth.OidcProvider.Options; using DysonNetwork.Pass.Auth.OidcProvider.Options;
using DysonNetwork.Pass.Auth.OidcProvider.Services; using DysonNetwork.Pass.Auth.OidcProvider.Services;
using DysonNetwork.Pass.Credit; using DysonNetwork.Pass.Credit;
@@ -22,7 +23,7 @@ using DysonNetwork.Pass.Realm;
using DysonNetwork.Pass.Safety; using DysonNetwork.Pass.Safety;
using DysonNetwork.Pass.Wallet.PaymentHandlers; using DysonNetwork.Pass.Wallet.PaymentHandlers;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.Geometry;
using DysonNetwork.Shared.Registry; using DysonNetwork.Shared.Registry;
namespace DysonNetwork.Pass.Startup; namespace DysonNetwork.Pass.Startup;
@@ -34,9 +35,7 @@ public static class ServiceCollectionExtensions
services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddDbContext<AppDatabase>(); services.AddDbContext<AppDatabase>();
services.AddSingleton<IClock>(SystemClock.Instance);
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddSingleton<ICacheService, CacheServiceRedis>();
services.AddHttpClient(); services.AddHttpClient();
@@ -136,8 +135,8 @@ public static class ServiceCollectionExtensions
IConfiguration configuration) IConfiguration configuration)
{ {
services.AddScoped<RazorViewRenderer>(); services.AddScoped<RazorViewRenderer>();
services.Configure<GeoIpOptions>(configuration.GetSection("GeoIP")); services.Configure<GeoOptions>(configuration.GetSection("GeoIP"));
services.AddScoped<GeoIpService>(); services.AddScoped<GeoService>();
services.AddScoped<EmailService>(); services.AddScoped<EmailService>();
services.AddScoped<PermissionService>(); services.AddScoped<PermissionService>();
services.AddScoped<ActionLogService>(); services.AddScoped<ActionLogService>();
@@ -159,6 +158,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<ExperienceService>(); services.AddScoped<ExperienceService>();
services.AddScoped<RealmService>(); services.AddScoped<RealmService>();
services.AddScoped<LotteryService>(); services.AddScoped<LotteryService>();
services.AddScoped<AffiliationSpellService>();
services.AddScoped<SpotifyPresenceService>(); services.AddScoped<SpotifyPresenceService>();
services.AddScoped<SteamPresenceService>(); services.AddScoped<SteamPresenceService>();

View File

@@ -588,13 +588,22 @@ public class PaymentService(
if (fund.RemainingAmount <= 0) if (fund.RemainingAmount <= 0)
return 0; return 0;
// For open mode funds: calculate amount per split // For open mode funds: use split type calculation
if (fund.IsOpen) if (fund.IsOpen)
{ {
// Calculate amount per split: TotalAmount / AmountOfSplits var remainingRecipients = fund.AmountOfSplits - fund.Recipients.Count(r => r.IsReceived);
var amountPerSplit = fund.TotalAmount / fund.AmountOfSplits; if (remainingRecipients == 0)
return Math.Max(amountPerSplit, 0.01m); // Minimum 0.01 per claim return 0;
var amount = fund.SplitType switch
{
Shared.Models.FundSplitType.Even => SplitEvenly(fund.RemainingAmount, remainingRecipients)[0],
Shared.Models.FundSplitType.Random => SplitRandomly(fund.RemainingAmount, remainingRecipients)[0],
_ => throw new ArgumentException("Invalid split type")
};
return Math.Max(amount, 0.01m);
} }
// For closed mode funds: use split type calculation // For closed mode funds: use split type calculation
var unclaimedRecipients = fund.Recipients.Count(r => !r.IsReceived); var unclaimedRecipients = fund.Recipients.Count(r => !r.IsReceived);

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Auth;
using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Permission;
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -196,7 +197,7 @@ public class WalletController(
[HttpPost("balance")] [HttpPost("balance")]
[Authorize] [Authorize]
[RequiredPermission("maintenance", "wallets.balance.modify")] [AskPermission("wallets.balance.modify")]
public async Task<ActionResult<SnWalletTransaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request) public async Task<ActionResult<SnWalletTransaction>> ModifyWalletBalance([FromBody] WalletBalanceRequest request)
{ {
var wallet = await ws.GetWalletAsync(request.AccountId); var wallet = await ws.GetWalletAsync(request.AccountId);

View File

@@ -15,7 +15,10 @@
"Authentication": { "Authentication": {
"Schemes": { "Schemes": {
"Bearer": { "Bearer": {
"ValidAudiences": ["http://localhost:5071", "https://localhost:7099"], "ValidAudiences": [
"http://localhost:5071",
"https://localhost:7099"
],
"ValidIssuer": "solar-network" "ValidIssuer": "solar-network"
} }
} }
@@ -59,6 +62,9 @@
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT" "DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
} }
}, },
"Cache": {
"Serializer": "MessagePack"
},
"Payment": { "Payment": {
"Auth": { "Auth": {
"Afdian": "<token here>" "Afdian": "<token here>"
@@ -71,5 +77,8 @@
} }
} }
}, },
"KnownProxies": ["127.0.0.1", "::1"] "KnownProxies": [
"127.0.0.1",
"::1"
]
} }

View File

@@ -4,7 +4,6 @@ using DysonNetwork.Shared.Stream;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NATS.Client.Core; using NATS.Client.Core;
using NATS.Net;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
using WebSocketPacket = DysonNetwork.Shared.Models.WebSocketPacket; using WebSocketPacket = DysonNetwork.Shared.Models.WebSocketPacket;
@@ -40,10 +39,10 @@ public class WebSocketController(
} }
var accountId = Guid.Parse(currentUser.Id!); var accountId = Guid.Parse(currentUser.Id!);
var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString(); var deviceId = currentSession.ClientId;
if (string.IsNullOrEmpty(deviceId)) if (string.IsNullOrEmpty(deviceId))
return BadRequest("Unable to get device ID from session."); deviceId = Guid.NewGuid().ToString();
if (deviceAlt is not null) if (deviceAlt is not null)
deviceId = $"{deviceId}+{deviceAlt}"; deviceId = $"{deviceId}+{deviceAlt}";

View File

@@ -93,7 +93,7 @@ public class NotificationController(
var result = var result =
await nty.SubscribeDevice( await nty.SubscribeDevice(
currentSession.Challenge.DeviceId, currentSession.ClientId,
request.DeviceToken, request.DeviceToken,
request.Provider, request.Provider,
currentUser currentUser
@@ -117,7 +117,7 @@ public class NotificationController(
var affectedRows = await db.PushSubscriptions var affectedRows = await db.PushSubscriptions
.Where(s => .Where(s =>
s.AccountId == accountId && s.AccountId == accountId &&
s.DeviceId == currentSession.Challenge.DeviceId s.DeviceId == currentSession.ClientId
).ExecuteDeleteAsync(); ).ExecuteDeleteAsync();
return Ok(affectedRows); return Ok(affectedRows);
} }
@@ -139,7 +139,7 @@ public class NotificationController(
[HttpPost("send")] [HttpPost("send")]
[Authorize] [Authorize]
[RequiredPermission("global", "notifications.send")] [AskPermission("notifications.send")]
public async Task<ActionResult> SendNotification( public async Task<ActionResult> SendNotification(
[FromBody] NotificationWithAimRequest request, [FromBody] NotificationWithAimRequest request,
[FromQuery] bool save = false [FromQuery] bool save = false

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