using DysonNetwork.Develop.Project; using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Proto; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; using System.Text; namespace DysonNetwork.Develop.Identity; public class CustomAppService( AppDatabase db, FileReferenceService.FileReferenceServiceClient fileRefs, FileService.FileServiceClient files ) { public async Task CreateAppAsync( Guid projectId, CustomAppController.CustomAppRequest request ) { var project = await db.DevProjects .Include(p => p.Developer) .FirstOrDefaultAsync(p => p.Id == projectId); if (project == null) return null; var app = new CustomApp { Slug = request.Slug!, Name = request.Name!, Description = request.Description, Status = request.Status ?? CustomAppStatus.Developing, Links = request.Links, OauthConfig = request.OauthConfig, ProjectId = projectId }; if (request.PictureId is not null) { var picture = await files.GetFileAsync( new GetFileRequest { Id = request.PictureId } ); if (picture is null) throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); app.Picture = CloudFileReferenceObject.FromProtoValue(picture); // Create a new reference await fileRefs.CreateReferenceAsync( new CreateReferenceRequest { FileId = picture.Id, Usage = "custom-apps.picture", ResourceId = app.ResourceIdentifier } ); } if (request.BackgroundId is not null) { var background = await files.GetFileAsync( new GetFileRequest { Id = request.BackgroundId } ); if (background is null) throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); app.Background = CloudFileReferenceObject.FromProtoValue(background); // Create a new reference await fileRefs.CreateReferenceAsync( new CreateReferenceRequest { FileId = background.Id, Usage = "custom-apps.background", ResourceId = app.ResourceIdentifier } ); } db.CustomApps.Add(app); await db.SaveChangesAsync(); return app; } public async Task GetAppAsync(Guid id, Guid? projectId = null) { var query = db.CustomApps.AsQueryable(); if (projectId.HasValue) { query = query.Where(a => a.ProjectId == projectId.Value); } return await query.FirstOrDefaultAsync(a => a.Id == id); } public async Task> GetAppSecretsAsync(Guid appId) { return await db.CustomAppSecrets .Where(s => s.AppId == appId) .OrderByDescending(s => s.CreatedAt) .ToListAsync(); } public async Task GetAppSecretAsync(Guid secretId, Guid appId) { return await db.CustomAppSecrets .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); } public async Task CreateAppSecretAsync(CustomAppSecret secret) { if (string.IsNullOrWhiteSpace(secret.Secret)) { // Generate a new random secret if not provided secret.Secret = GenerateRandomSecret(); } secret.Id = Guid.NewGuid(); secret.CreatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(); secret.UpdatedAt = secret.CreatedAt; db.CustomAppSecrets.Add(secret); await db.SaveChangesAsync(); return secret; } public async Task DeleteAppSecretAsync(Guid secretId, Guid appId) { var secret = await db.CustomAppSecrets .FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId); if (secret == null) return false; db.CustomAppSecrets.Remove(secret); await db.SaveChangesAsync(); return true; } public async Task RotateAppSecretAsync(CustomAppSecret secretUpdate) { var existingSecret = await db.CustomAppSecrets .FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId); if (existingSecret == null) throw new InvalidOperationException("Secret not found"); // Update the existing secret with new values existingSecret.Secret = GenerateRandomSecret(); existingSecret.Description = secretUpdate.Description ?? existingSecret.Description; existingSecret.ExpiredAt = secretUpdate.ExpiredAt ?? existingSecret.ExpiredAt; existingSecret.IsOidc = secretUpdate.IsOidc; existingSecret.UpdatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(); await db.SaveChangesAsync(); return existingSecret; } private static string GenerateRandomSecret(int length = 64) { const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~+"; var res = new StringBuilder(); using (var rng = RandomNumberGenerator.Create()) { var uintBuffer = new byte[sizeof(uint)]; while (length-- > 0) { rng.GetBytes(uintBuffer); var num = BitConverter.ToUInt32(uintBuffer, 0); res.Append(valid[(int)(num % (uint)valid.Length)]); } } return res.ToString(); } public async Task> GetAppsByProjectAsync(Guid projectId) { return await db.CustomApps .Where(a => a.ProjectId == projectId) .ToListAsync(); } public async Task UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request) { if (request.Slug is not null) app.Slug = request.Slug; if (request.Name is not null) app.Name = request.Name; if (request.Description is not null) app.Description = request.Description; if (request.Status is not null) app.Status = request.Status.Value; if (request.Links is not null) app.Links = request.Links; if (request.OauthConfig is not null) app.OauthConfig = request.OauthConfig; if (request.PictureId is not null) { var picture = await files.GetFileAsync( new GetFileRequest { Id = request.PictureId } ); if (picture is null) throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); app.Picture = CloudFileReferenceObject.FromProtoValue(picture); // Create a new reference await fileRefs.CreateReferenceAsync( new CreateReferenceRequest { FileId = picture.Id, Usage = "custom-apps.picture", ResourceId = app.ResourceIdentifier } ); } if (request.BackgroundId is not null) { var background = await files.GetFileAsync( new GetFileRequest { Id = request.BackgroundId } ); if (background is null) throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud."); app.Background = CloudFileReferenceObject.FromProtoValue(background); // Create a new reference await fileRefs.CreateReferenceAsync( new CreateReferenceRequest { FileId = background.Id, Usage = "custom-apps.background", ResourceId = app.ResourceIdentifier } ); } db.Update(app); await db.SaveChangesAsync(); return app; } public async Task DeleteAppAsync(Guid id) { var app = await db.CustomApps.FindAsync(id); if (app == null) { return false; } db.CustomApps.Remove(app); await db.SaveChangesAsync(); await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest { ResourceId = app.ResourceIdentifier } ); return true; } }