mirror of
https://github.com/azaion/annotations.git
synced 2026-04-23 12:26:30 +00:00
Refactor annotation tool from WPF desktop app to .NET API
Replace the WPF desktop application (Azaion.Suite, Azaion.Annotator, Azaion.Common, Azaion.Inference, Azaion.Loader, Azaion.LoaderUI, Azaion.Dataset, Azaion.Test) with a standalone .NET Web API in src/. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
using System.Threading.Channels;
|
||||
using Azaion.Annotations.DTOs;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class AnnotationEventService
|
||||
{
|
||||
private readonly Channel<AnnotationEventDto> _channel = Channel.CreateUnbounded<AnnotationEventDto>();
|
||||
|
||||
public ChannelReader<AnnotationEventDto> Reader => _channel.Reader;
|
||||
|
||||
public async ValueTask PublishAsync(AnnotationEventDto evt)
|
||||
{
|
||||
await _channel.Writer.WriteAsync(evt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
using System.IO.Hashing;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using Azaion.Annotations.Database;
|
||||
using Azaion.Annotations.Database.Entities;
|
||||
using Azaion.Annotations.DTOs;
|
||||
using Azaion.Annotations.Enums;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class AnnotationService(AppDataConnection db, PathResolver pathResolver, AnnotationEventService events)
|
||||
{
|
||||
public async Task<Annotation> CreateAnnotation(CreateAnnotationRequest request, Guid userId)
|
||||
{
|
||||
string id;
|
||||
|
||||
if (request.Image is { Length: > 0 })
|
||||
{
|
||||
id = ComputeHash(request.Image);
|
||||
var imgPath = await pathResolver.GetImagePath(id);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(imgPath)!);
|
||||
await File.WriteAllBytesAsync(imgPath, request.Image);
|
||||
|
||||
var existingMedia = await db.Media.FirstOrDefaultAsync(m => m.Id == id);
|
||||
if (existingMedia == null)
|
||||
{
|
||||
await db.InsertAsync(new Media
|
||||
{
|
||||
Id = id,
|
||||
Name = id,
|
||||
Path = imgPath,
|
||||
MediaType = MediaType.Image,
|
||||
MediaStatus = MediaStatus.New,
|
||||
WaypointId = request.WaypointId,
|
||||
UserId = userId
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(request.MediaId))
|
||||
{
|
||||
var media = await db.Media.FirstOrDefaultAsync(m => m.Id == request.MediaId)
|
||||
?? throw new KeyNotFoundException($"Media {request.MediaId} not found");
|
||||
id = request.MediaId;
|
||||
var imgPath = await pathResolver.GetImagePath(id);
|
||||
if (File.Exists(media.Path) && !File.Exists(imgPath))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(imgPath)!);
|
||||
File.Copy(media.Path, imgPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Provide either Image bytes or MediaId");
|
||||
}
|
||||
|
||||
var annotation = new Annotation
|
||||
{
|
||||
Id = id,
|
||||
MediaId = id,
|
||||
Time = request.VideoTime,
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
UserId = userId,
|
||||
Source = request.Source,
|
||||
Status = AnnotationStatus.Created
|
||||
};
|
||||
|
||||
await db.InsertAsync(annotation);
|
||||
|
||||
var detections = request.Detections.Select(d => new Detection
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CenterX = d.CenterX,
|
||||
CenterY = d.CenterY,
|
||||
Width = d.Width,
|
||||
Height = d.Height,
|
||||
ClassNum = d.ClassNum,
|
||||
Label = d.Label,
|
||||
Description = d.Description,
|
||||
Confidence = d.Confidence,
|
||||
Affiliation = d.Affiliation,
|
||||
CombatReadiness = d.CombatReadiness,
|
||||
AnnotationId = id
|
||||
}).ToList();
|
||||
|
||||
if (detections.Count > 0)
|
||||
await db.BulkCopyAsync(detections);
|
||||
|
||||
await WriteLabelFile(id, request.Detections);
|
||||
|
||||
await events.PublishAsync(new AnnotationEventDto
|
||||
{
|
||||
AnnotationId = id,
|
||||
MediaId = id,
|
||||
Status = AnnotationStatus.Created,
|
||||
Source = request.Source,
|
||||
Detections = request.Detections,
|
||||
CreatedDate = annotation.CreatedDate
|
||||
});
|
||||
|
||||
var settings = await db.SystemSettings.FirstOrDefaultAsync();
|
||||
if (settings is not { SilentDetection: true })
|
||||
await FailsafeProducer.EnqueueAsync(db, id, QueueOperation.Created);
|
||||
|
||||
return annotation;
|
||||
}
|
||||
|
||||
public async Task UpdateAnnotation(string id, UpdateAnnotationRequest request)
|
||||
{
|
||||
var exists = await db.Annotations.AnyAsync(a => a.Id == id);
|
||||
if (!exists) throw new KeyNotFoundException($"Annotation {id} not found");
|
||||
|
||||
await db.Detections.DeleteAsync(d => d.AnnotationId == id);
|
||||
|
||||
var detections = request.Detections.Select(d => new Detection
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CenterX = d.CenterX,
|
||||
CenterY = d.CenterY,
|
||||
Width = d.Width,
|
||||
Height = d.Height,
|
||||
ClassNum = d.ClassNum,
|
||||
Label = d.Label,
|
||||
Description = d.Description,
|
||||
Confidence = d.Confidence,
|
||||
Affiliation = d.Affiliation,
|
||||
CombatReadiness = d.CombatReadiness,
|
||||
AnnotationId = id
|
||||
}).ToList();
|
||||
|
||||
if (detections.Count > 0)
|
||||
await db.BulkCopyAsync(detections);
|
||||
|
||||
await db.Annotations
|
||||
.Where(a => a.Id == id)
|
||||
.Set(a => a.Status, AnnotationStatus.Edited)
|
||||
.UpdateAsync();
|
||||
|
||||
await WriteLabelFile(id, request.Detections);
|
||||
}
|
||||
|
||||
public async Task UpdateStatus(string id, AnnotationStatus status)
|
||||
{
|
||||
var updated = await db.Annotations
|
||||
.Where(a => a.Id == id)
|
||||
.Set(a => a.Status, status)
|
||||
.UpdateAsync();
|
||||
|
||||
if (updated == 0) throw new KeyNotFoundException($"Annotation {id} not found");
|
||||
}
|
||||
|
||||
public async Task DeleteAnnotation(string id)
|
||||
{
|
||||
var exists = await db.Annotations.AnyAsync(a => a.Id == id);
|
||||
if (!exists) throw new KeyNotFoundException($"Annotation {id} not found");
|
||||
|
||||
await db.Detections.DeleteAsync(d => d.AnnotationId == id);
|
||||
await db.Annotations.DeleteAsync(a => a.Id == id);
|
||||
|
||||
await DeleteFiles(id);
|
||||
}
|
||||
|
||||
public async Task<PaginatedResponse<AnnotationListItem>> GetAnnotations(GetAnnotationsQuery query)
|
||||
{
|
||||
var q = db.Annotations.AsQueryable();
|
||||
|
||||
if (query.From.HasValue)
|
||||
q = q.Where(a => a.CreatedDate >= query.From.Value);
|
||||
if (query.To.HasValue)
|
||||
q = q.Where(a => a.CreatedDate <= query.To.Value);
|
||||
if (query.UserId.HasValue)
|
||||
q = q.Where(a => a.UserId == query.UserId.Value);
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(a => db.Media.Any(m => m.Id == a.MediaId && m.Name.ToLower().Contains(query.Name.ToLower())));
|
||||
if (!string.IsNullOrEmpty(query.MediaId))
|
||||
q = q.Where(a => a.MediaId == query.MediaId);
|
||||
if (query.FlightId.HasValue)
|
||||
q = q.Where(a => db.Media.Any(m => m.Id == a.MediaId && m.WaypointId != null));
|
||||
|
||||
var totalCount = await q.CountAsync();
|
||||
|
||||
var annotations = await q
|
||||
.OrderByDescending(a => a.CreatedDate)
|
||||
.Skip((query.Page - 1) * query.PageSize)
|
||||
.Take(query.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var annotationIds = annotations.Select(a => a.Id).ToList();
|
||||
var detections = await db.Detections
|
||||
.Where(d => annotationIds.Contains(d.AnnotationId))
|
||||
.ToListAsync();
|
||||
var detectionsByAnnotation = detections.GroupBy(d => d.AnnotationId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
var items = annotations.Select(a =>
|
||||
{
|
||||
detectionsByAnnotation.TryGetValue(a.Id, out var dets);
|
||||
return new AnnotationListItem
|
||||
{
|
||||
Id = a.Id,
|
||||
MediaId = a.MediaId,
|
||||
Time = TimeSpan.FromTicks(a.TimeTicks).ToString(@"hh\:mm\:ss"),
|
||||
CreatedDate = a.CreatedDate,
|
||||
UserId = a.UserId,
|
||||
Source = a.Source,
|
||||
Status = a.Status,
|
||||
IsSplit = a.IsSplit,
|
||||
SplitTile = a.SplitTile,
|
||||
Detections = (dets ?? []).Select(d => new DetectionListDto
|
||||
{
|
||||
Id = d.Id,
|
||||
ClassNum = d.ClassNum,
|
||||
Label = d.Label,
|
||||
Confidence = d.Confidence,
|
||||
Affiliation = d.Affiliation,
|
||||
CombatReadiness = d.CombatReadiness,
|
||||
CenterX = d.CenterX,
|
||||
CenterY = d.CenterY,
|
||||
Width = d.Width,
|
||||
Height = d.Height
|
||||
}).ToList()
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new PaginatedResponse<AnnotationListItem>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Annotation> GetAnnotation(string id)
|
||||
{
|
||||
var annotation = await db.Annotations
|
||||
.LoadWith(a => a.Detections)
|
||||
.Where(a => a.Id == id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return annotation ?? throw new KeyNotFoundException($"Annotation {id} not found");
|
||||
}
|
||||
|
||||
private async Task WriteLabelFile(string annotationId, List<DetectionDto> detections)
|
||||
{
|
||||
var labelPath = await pathResolver.GetLabelPath(annotationId);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(labelPath)!);
|
||||
var lines = detections.Select(d => $"{d.ClassNum} {d.CenterX} {d.CenterY} {d.Width} {d.Height}");
|
||||
await File.WriteAllLinesAsync(labelPath, lines);
|
||||
}
|
||||
|
||||
private async Task DeleteFiles(string annotationId)
|
||||
{
|
||||
var paths = new[]
|
||||
{
|
||||
await pathResolver.GetImagePath(annotationId),
|
||||
await pathResolver.GetLabelPath(annotationId),
|
||||
await pathResolver.GetThumbnailPath(annotationId)
|
||||
};
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] data)
|
||||
{
|
||||
byte[] input;
|
||||
if (data.Length > 3072)
|
||||
{
|
||||
var buffer = new byte[8 + 3072];
|
||||
BitConverter.GetBytes((long)data.Length).CopyTo(buffer, 0);
|
||||
Array.Copy(data, 0, buffer, 8, 1024);
|
||||
Array.Copy(data, data.Length / 2 - 512, buffer, 8 + 1024, 1024);
|
||||
Array.Copy(data, data.Length - 1024, buffer, 8 + 2048, 1024);
|
||||
input = buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = new byte[8 + data.Length];
|
||||
BitConverter.GetBytes((long)data.Length).CopyTo(buffer, 0);
|
||||
Array.Copy(data, 0, buffer, 8, data.Length);
|
||||
input = buffer;
|
||||
}
|
||||
var hash = XxHash64.Hash(input);
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using LinqToDB;
|
||||
using Azaion.Annotations.Database;
|
||||
using Azaion.Annotations.Database.Entities;
|
||||
using Azaion.Annotations.DTOs;
|
||||
using Azaion.Annotations.Enums;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class DatasetService(AppDataConnection db, PathResolver pathResolver)
|
||||
{
|
||||
public async Task<PaginatedResponse<DatasetItem>> GetDataset(GetDatasetQuery query)
|
||||
{
|
||||
var q = db.Annotations.AsQueryable();
|
||||
|
||||
if (query.FromDate.HasValue)
|
||||
q = q.Where(a => a.CreatedDate >= query.FromDate.Value);
|
||||
if (query.ToDate.HasValue)
|
||||
q = q.Where(a => a.CreatedDate <= query.ToDate.Value);
|
||||
if (query.FlightId.HasValue)
|
||||
q = q.Where(a => db.Media.Any(m => m.Id == a.MediaId && m.WaypointId != null));
|
||||
if (query.Status.HasValue)
|
||||
q = q.Where(a => a.Status == query.Status.Value);
|
||||
if (query.ClassNum.HasValue)
|
||||
q = q.Where(a => db.Detections.Any(d => d.AnnotationId == a.Id && d.ClassNum == query.ClassNum.Value));
|
||||
if (query.HasDetections == true)
|
||||
q = q.Where(a => db.Detections.Any(d => d.AnnotationId == a.Id));
|
||||
if (query.HasDetections == false)
|
||||
q = q.Where(a => !db.Detections.Any(d => d.AnnotationId == a.Id));
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(a => a.Id.ToLower().Contains(query.Name.ToLower()));
|
||||
|
||||
var totalCount = await q.CountAsync();
|
||||
|
||||
var annotations = await q
|
||||
.OrderByDescending(a => a.CreatedDate)
|
||||
.Skip((query.Page - 1) * query.PageSize)
|
||||
.Take(query.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var items = new List<DatasetItem>();
|
||||
foreach (var a in annotations)
|
||||
{
|
||||
items.Add(new DatasetItem
|
||||
{
|
||||
AnnotationId = a.Id,
|
||||
ImageName = $"{a.Id}.jpg",
|
||||
ThumbnailPath = await pathResolver.GetThumbnailPath(a.Id),
|
||||
Status = a.Status,
|
||||
CreatedDate = a.CreatedDate,
|
||||
Source = a.Source,
|
||||
IsSplit = a.IsSplit,
|
||||
IsSeed = a.Source == AnnotationSource.AI && a.Status == AnnotationStatus.Validated
|
||||
});
|
||||
}
|
||||
|
||||
return new PaginatedResponse<DatasetItem>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Annotation> GetAnnotationDetail(string annotationId)
|
||||
{
|
||||
var annotation = await db.Annotations
|
||||
.LoadWith(a => a.Detections)
|
||||
.Where(a => a.Id == annotationId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return annotation ?? throw new KeyNotFoundException($"Annotation {annotationId} not found");
|
||||
}
|
||||
|
||||
public async Task UpdateStatus(string annotationId, AnnotationStatus status)
|
||||
{
|
||||
var updated = await db.Annotations
|
||||
.Where(a => a.Id == annotationId)
|
||||
.Set(a => a.Status, status)
|
||||
.UpdateAsync();
|
||||
|
||||
if (updated == 0) throw new KeyNotFoundException($"Annotation {annotationId} not found");
|
||||
}
|
||||
|
||||
public async Task BulkUpdateStatus(BulkStatusRequest request)
|
||||
{
|
||||
if (request.AnnotationIds.Count == 0)
|
||||
throw new ArgumentException("Empty annotationIds list");
|
||||
|
||||
await db.Annotations
|
||||
.Where(a => request.AnnotationIds.Contains(a.Id))
|
||||
.Set(a => a.Status, request.Status)
|
||||
.UpdateAsync();
|
||||
}
|
||||
|
||||
public async Task<List<ClassDistributionItem>> GetClassDistribution()
|
||||
{
|
||||
var classes = await db.DetectionClasses.ToListAsync();
|
||||
var classMap = classes.ToDictionary(c => c.Id, c => c);
|
||||
|
||||
var groups = await db.Detections
|
||||
.GroupBy(d => d.ClassNum)
|
||||
.Select(g => new { ClassNum = g.Key, Count = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
return groups.Select(g =>
|
||||
{
|
||||
classMap.TryGetValue(g.ClassNum, out var cls);
|
||||
return new ClassDistributionItem
|
||||
{
|
||||
ClassNum = g.ClassNum,
|
||||
Label = cls?.Name ?? $"Class {g.ClassNum}",
|
||||
Color = cls?.Color ?? "#888888",
|
||||
Count = g.Count
|
||||
};
|
||||
}).OrderByDescending(x => x.Count).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using LinqToDB;
|
||||
using Azaion.Annotations.Database;
|
||||
using Azaion.Annotations.Database.Entities;
|
||||
using Azaion.Annotations.DTOs;
|
||||
using Azaion.Annotations.Enums;
|
||||
using MessagePack;
|
||||
using RabbitMQ.Stream.Client;
|
||||
using RabbitMQ.Stream.Client.AMQP;
|
||||
using RabbitMQ.Stream.Client.Reliable;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class RabbitMqConfig
|
||||
{
|
||||
public string Host { get; set; } = "rabbitmq";
|
||||
public int Port { get; set; } = 5552;
|
||||
public string Username { get; set; } = "azaion_producer";
|
||||
public string Password { get; set; } = "producer_pass";
|
||||
public string StreamName { get; set; } = "azaion-annotations";
|
||||
}
|
||||
|
||||
public class FailsafeProducer(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
PathResolver pathResolver,
|
||||
RabbitMqConfig config,
|
||||
ILogger<FailsafeProducer> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), ct);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessQueue(ct);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, ex.Message);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessQueue(CancellationToken ct)
|
||||
{
|
||||
var streamSystem = await StreamSystem.Create(new StreamSystemConfig
|
||||
{
|
||||
Endpoints = [new IPEndPoint(IPAddress.Parse(config.Host), config.Port)],
|
||||
UserName = config.Username,
|
||||
Password = config.Password
|
||||
});
|
||||
|
||||
var producer = await Producer.Create(new ProducerConfig(streamSystem, config.StreamName));
|
||||
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
await DrainQueue(producer, ct);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), ct);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await producer.Close();
|
||||
await streamSystem.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DrainQueue(Producer producer, CancellationToken ct)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDataConnection>();
|
||||
|
||||
var records = await db.AnnotationsQueueRecords
|
||||
.OrderBy(x => x.DateTime)
|
||||
.ToListAsync(token: ct);
|
||||
|
||||
if (records.Count == 0)
|
||||
return;
|
||||
|
||||
var createdIds = records
|
||||
.Where(x => x.Operation == QueueOperation.Created)
|
||||
.SelectMany(x => ParseIds(x.AnnotationIds))
|
||||
.ToList();
|
||||
|
||||
var annotationsDict = createdIds.Count > 0
|
||||
? await db.Annotations
|
||||
.LoadWith(a => a.Detections)
|
||||
.Where(a => createdIds.Contains(a.Id))
|
||||
.ToDictionaryAsync(a => a.Id, ct)
|
||||
: new Dictionary<string, Annotation>();
|
||||
|
||||
var messages = new List<Message>();
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
var ids = ParseIds(record.AnnotationIds);
|
||||
|
||||
if (record.Operation is QueueOperation.Validated or QueueOperation.Deleted)
|
||||
{
|
||||
var msg = MessagePackSerializer.Serialize(new AnnotationBulkQueueMessage
|
||||
{
|
||||
AnnotationIds = ids.ToArray(),
|
||||
Operation = (int)record.Operation,
|
||||
CreatedDate = record.DateTime
|
||||
});
|
||||
messages.Add(new Message(msg)
|
||||
{
|
||||
ApplicationProperties = new ApplicationProperties
|
||||
{
|
||||
{ "Operation", record.Operation.ToString() }
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (!annotationsDict.TryGetValue(id, out var annotation))
|
||||
continue;
|
||||
|
||||
byte[]? image = null;
|
||||
try
|
||||
{
|
||||
var imgPath = await pathResolver.GetImagePath(id);
|
||||
if (File.Exists(imgPath))
|
||||
image = await File.ReadAllBytesAsync(imgPath, ct);
|
||||
}
|
||||
catch { }
|
||||
|
||||
var detectionsJson = JsonSerializer.Serialize(
|
||||
annotation.Detections?.Select(d => new
|
||||
{
|
||||
d.CenterX, d.CenterY, d.Width, d.Height,
|
||||
d.ClassNum, d.Label, d.Confidence
|
||||
}) ?? []);
|
||||
|
||||
var msg = MessagePackSerializer.Serialize(new AnnotationQueueMessage
|
||||
{
|
||||
Name = annotation.Id,
|
||||
MediaHash = annotation.MediaId,
|
||||
OriginalMediaName = annotation.MediaId,
|
||||
Time = TimeSpan.FromTicks(annotation.TimeTicks),
|
||||
ImageExtension = ".jpg",
|
||||
Detections = detectionsJson,
|
||||
Image = image,
|
||||
Email = "",
|
||||
Source = (int)annotation.Source,
|
||||
Status = (int)annotation.Status,
|
||||
CreatedDate = annotation.CreatedDate
|
||||
});
|
||||
|
||||
messages.Add(new Message(msg)
|
||||
{
|
||||
ApplicationProperties = new ApplicationProperties
|
||||
{
|
||||
{ "Operation", record.Operation.ToString() }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.Count > 0)
|
||||
{
|
||||
await producer.Send(messages, CompressionType.Gzip);
|
||||
var recordIds = records.Select(x => x.Id).ToList();
|
||||
await db.AnnotationsQueueRecords
|
||||
.Where(x => recordIds.Contains(x.Id))
|
||||
.DeleteAsync(token: ct);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ParseIds(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnqueueAsync(AppDataConnection db, string annotationId, QueueOperation operation)
|
||||
{
|
||||
var ids = JsonSerializer.Serialize(new[] { annotationId });
|
||||
await db.InsertAsync(new AnnotationsQueueRecord
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DateTime = DateTime.UtcNow,
|
||||
Operation = operation,
|
||||
AnnotationIds = ids
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO.Hashing;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using Azaion.Annotations.Database;
|
||||
using Azaion.Annotations.Database.Entities;
|
||||
using Azaion.Annotations.DTOs;
|
||||
using Azaion.Annotations.Enums;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class MediaService(AppDataConnection db, PathResolver pathResolver)
|
||||
{
|
||||
public async Task<Media> CreateMedia(CreateMediaRequest request, Guid userId)
|
||||
{
|
||||
var id = request.Data is { Length: > 0 }
|
||||
? ComputeHash(request.Data)
|
||||
: ComputeHash(System.Text.Encoding.UTF8.GetBytes(request.Path));
|
||||
|
||||
var media = new Media
|
||||
{
|
||||
Id = id,
|
||||
Name = request.Name,
|
||||
Path = request.Path,
|
||||
MediaType = request.MediaType,
|
||||
MediaStatus = MediaStatus.New,
|
||||
WaypointId = request.WaypointId,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
if (request.Data is { Length: > 0 })
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(request.Path)!);
|
||||
await File.WriteAllBytesAsync(request.Path, request.Data);
|
||||
}
|
||||
|
||||
if (media.MediaType == MediaType.Video)
|
||||
media.Duration = await ExtractDuration(media.Path);
|
||||
|
||||
await db.InsertAsync(media);
|
||||
return media;
|
||||
}
|
||||
|
||||
public async Task<List<Media>> CreateMediaBatch(Guid waypointId, IFormFileCollection files, Guid userId)
|
||||
{
|
||||
if (files.Count == 0) throw new ArgumentException("No files provided");
|
||||
|
||||
var mediaDir = await pathResolver.GetMediaDir();
|
||||
Directory.CreateDirectory(mediaDir);
|
||||
|
||||
var mediaList = new List<Media>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms);
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
var id = ComputeHash(bytes);
|
||||
var filePath = Path.Combine(mediaDir, $"{id}{Path.GetExtension(file.FileName)}");
|
||||
await File.WriteAllBytesAsync(filePath, bytes);
|
||||
|
||||
var mediaType = ResolveMediaType(file.ContentType, file.FileName);
|
||||
var media = new Media
|
||||
{
|
||||
Id = id,
|
||||
Name = file.FileName,
|
||||
Path = filePath,
|
||||
MediaType = mediaType,
|
||||
MediaStatus = MediaStatus.New,
|
||||
WaypointId = waypointId,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
if (mediaType == MediaType.Video)
|
||||
media.Duration = await ExtractDuration(filePath);
|
||||
|
||||
mediaList.Add(media);
|
||||
}
|
||||
|
||||
await db.BulkCopyAsync(mediaList);
|
||||
return mediaList;
|
||||
}
|
||||
|
||||
private static MediaType ResolveMediaType(string contentType, string fileName)
|
||||
{
|
||||
if (contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
|
||||
return MediaType.Video;
|
||||
if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
return MediaType.Image;
|
||||
|
||||
var ext = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return ext switch
|
||||
{
|
||||
".mp4" or ".avi" or ".mov" or ".mkv" => MediaType.Video,
|
||||
".jpg" or ".jpeg" or ".png" or ".bmp" or ".tiff" => MediaType.Image,
|
||||
_ => MediaType.None
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Media?> GetMediaById(string id)
|
||||
{
|
||||
return await db.Media.FirstOrDefaultAsync(m => m.Id == id);
|
||||
}
|
||||
|
||||
public async Task<PaginatedResponse<MediaListItem>> GetMedia(GetMediaQuery query)
|
||||
{
|
||||
var q = db.Media.AsQueryable();
|
||||
|
||||
if (query.FlightId.HasValue)
|
||||
q = q.Where(m => m.WaypointId != null);
|
||||
if (!string.IsNullOrEmpty(query.Name))
|
||||
q = q.Where(m => m.Name.ToLower().Contains(query.Name.ToLower()));
|
||||
if (!string.IsNullOrEmpty(query.Path))
|
||||
q = q.Where(m => m.Path.ToLower().Contains(query.Path.ToLower()));
|
||||
|
||||
var totalCount = await q.CountAsync();
|
||||
|
||||
var items = await q
|
||||
.OrderByDescending(m => m.Id)
|
||||
.Skip((query.Page - 1) * query.PageSize)
|
||||
.Take(query.PageSize)
|
||||
.Select(m => new MediaListItem
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name,
|
||||
Path = m.Path,
|
||||
MediaType = m.MediaType,
|
||||
MediaStatus = m.MediaStatus,
|
||||
Duration = m.Duration,
|
||||
AnnotationCount = db.Annotations.Count(a => a.MediaId == m.Id),
|
||||
WaypointId = m.WaypointId,
|
||||
UserId = m.UserId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new PaginatedResponse<MediaListItem>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteMedia(string id)
|
||||
{
|
||||
var media = await db.Media.FirstOrDefaultAsync(m => m.Id == id)
|
||||
?? throw new KeyNotFoundException($"Media {id} not found");
|
||||
|
||||
var annotationIds = await db.Annotations
|
||||
.Where(a => a.MediaId == id)
|
||||
.Select(a => a.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (annotationIds.Count > 0)
|
||||
{
|
||||
await db.Detections.DeleteAsync(d => annotationIds.Contains(d.AnnotationId));
|
||||
await db.Annotations.DeleteAsync(a => a.MediaId == id);
|
||||
|
||||
foreach (var annId in annotationIds)
|
||||
{
|
||||
var paths = new[]
|
||||
{
|
||||
await pathResolver.GetImagePath(annId),
|
||||
await pathResolver.GetLabelPath(annId),
|
||||
await pathResolver.GetThumbnailPath(annId)
|
||||
};
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.Media.DeleteAsync(m => m.Id == id);
|
||||
|
||||
if (File.Exists(media.Path))
|
||||
File.Delete(media.Path);
|
||||
}
|
||||
|
||||
private static string ComputeHash(byte[] data)
|
||||
{
|
||||
byte[] input;
|
||||
if (data.Length > 3072)
|
||||
{
|
||||
var buffer = new byte[8 + 3072];
|
||||
BitConverter.GetBytes((long)data.Length).CopyTo(buffer, 0);
|
||||
Array.Copy(data, 0, buffer, 8, 1024);
|
||||
Array.Copy(data, data.Length / 2 - 512, buffer, 8 + 1024, 1024);
|
||||
Array.Copy(data, data.Length - 1024, buffer, 8 + 2048, 1024);
|
||||
input = buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
var buffer = new byte[8 + data.Length];
|
||||
BitConverter.GetBytes((long)data.Length).CopyTo(buffer, 0);
|
||||
Array.Copy(data, 0, buffer, 8, data.Length);
|
||||
input = buffer;
|
||||
}
|
||||
var hash = XxHash64.Hash(input);
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
private static async Task<string?> ExtractDuration(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffprobe",
|
||||
Arguments = $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{filePath}\"",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
process.Start();
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (double.TryParse(output.Trim(), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var seconds))
|
||||
{
|
||||
return TimeSpan.FromSeconds(seconds).ToString(@"hh\:mm\:ss");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using LinqToDB;
|
||||
using Azaion.Annotations.Database;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class PathResolver(AppDataConnection db)
|
||||
{
|
||||
private string _videosDir = "/data/videos";
|
||||
private string _imagesDir = "/data/images";
|
||||
private string _labelsDir = "/data/labels";
|
||||
private string _resultsDir = "/data/results";
|
||||
private string _thumbnailsDir = "/data/thumbnails";
|
||||
private bool _initialized;
|
||||
|
||||
private async Task EnsureInitialized()
|
||||
{
|
||||
if (_initialized) return;
|
||||
var dirs = await db.DirectorySettings.FirstOrDefaultAsync();
|
||||
if (dirs != null)
|
||||
{
|
||||
_videosDir = dirs.VideosDir;
|
||||
_imagesDir = dirs.ImagesDir;
|
||||
_labelsDir = dirs.LabelsDir;
|
||||
_resultsDir = dirs.ResultsDir;
|
||||
_thumbnailsDir = dirs.ThumbnailsDir;
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public async Task<string> GetImagePath(string annotationId)
|
||||
{
|
||||
await EnsureInitialized();
|
||||
return Path.Combine(_imagesDir, $"{annotationId}.jpg");
|
||||
}
|
||||
|
||||
public async Task<string> GetLabelPath(string annotationId)
|
||||
{
|
||||
await EnsureInitialized();
|
||||
return Path.Combine(_labelsDir, $"{annotationId}.txt");
|
||||
}
|
||||
|
||||
public async Task<string> GetThumbnailPath(string annotationId)
|
||||
{
|
||||
await EnsureInitialized();
|
||||
return Path.Combine(_thumbnailsDir, $"{annotationId}.jpg");
|
||||
}
|
||||
|
||||
public async Task<string> GetResultPath(string annotationId)
|
||||
{
|
||||
await EnsureInitialized();
|
||||
return Path.Combine(_resultsDir, $"{annotationId}_result.jpg");
|
||||
}
|
||||
|
||||
public async Task<string> GetMediaDir()
|
||||
{
|
||||
await EnsureInitialized();
|
||||
return _videosDir;
|
||||
}
|
||||
|
||||
public void Reset() => _initialized = false;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using LinqToDB;
|
||||
using Azaion.Annotations.Database;
|
||||
using Azaion.Annotations.Database.Entities;
|
||||
using Azaion.Annotations.DTOs;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class SettingsService(AppDataConnection db, PathResolver pathResolver)
|
||||
{
|
||||
public async Task<SystemSettings?> GetSystemSettings()
|
||||
{
|
||||
return await db.SystemSettings.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateSystemSettings(UpdateSystemSettingsRequest request)
|
||||
{
|
||||
var existing = await db.SystemSettings.FirstOrDefaultAsync();
|
||||
if (existing == null)
|
||||
{
|
||||
await db.InsertAsync(new SystemSettings
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = request.Name,
|
||||
MilitaryUnit = request.MilitaryUnit,
|
||||
DefaultCameraWidth = request.DefaultCameraWidth,
|
||||
DefaultCameraFoV = request.DefaultCameraFoV,
|
||||
ThumbnailWidth = request.ThumbnailWidth ?? 240,
|
||||
ThumbnailHeight = request.ThumbnailHeight ?? 135,
|
||||
ThumbnailBorder = request.ThumbnailBorder ?? 10,
|
||||
GenerateAnnotatedImage = request.GenerateAnnotatedImage ?? false,
|
||||
SilentDetection = request.SilentDetection ?? false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.SystemSettings
|
||||
.Where(x => x.Id == existing.Id)
|
||||
.Set(x => x.Name, request.Name ?? existing.Name)
|
||||
.Set(x => x.MilitaryUnit, request.MilitaryUnit ?? existing.MilitaryUnit)
|
||||
.Set(x => x.DefaultCameraWidth, request.DefaultCameraWidth ?? existing.DefaultCameraWidth)
|
||||
.Set(x => x.DefaultCameraFoV, request.DefaultCameraFoV ?? existing.DefaultCameraFoV)
|
||||
.Set(x => x.ThumbnailWidth, request.ThumbnailWidth ?? existing.ThumbnailWidth)
|
||||
.Set(x => x.ThumbnailHeight, request.ThumbnailHeight ?? existing.ThumbnailHeight)
|
||||
.Set(x => x.ThumbnailBorder, request.ThumbnailBorder ?? existing.ThumbnailBorder)
|
||||
.Set(x => x.GenerateAnnotatedImage, request.GenerateAnnotatedImage ?? existing.GenerateAnnotatedImage)
|
||||
.Set(x => x.SilentDetection, request.SilentDetection ?? existing.SilentDetection)
|
||||
.UpdateAsync();
|
||||
}
|
||||
|
||||
public async Task<DirectorySettings?> GetDirectorySettings()
|
||||
{
|
||||
return await db.DirectorySettings.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateDirectorySettings(UpdateDirectoriesRequest request)
|
||||
{
|
||||
var existing = await db.DirectorySettings.FirstOrDefaultAsync();
|
||||
if (existing == null)
|
||||
{
|
||||
await db.InsertAsync(new DirectorySettings
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
VideosDir = request.VideosDir ?? "/data/videos",
|
||||
ImagesDir = request.ImagesDir ?? "/data/images",
|
||||
LabelsDir = request.LabelsDir ?? "/data/labels",
|
||||
ResultsDir = request.ResultsDir ?? "/data/results",
|
||||
ThumbnailsDir = request.ThumbnailsDir ?? "/data/thumbnails",
|
||||
GpsSatDir = request.GpsSatDir ?? "/data/gps_sat",
|
||||
GpsRouteDir = request.GpsRouteDir ?? "/data/gps_route"
|
||||
});
|
||||
pathResolver.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await db.DirectorySettings
|
||||
.Where(x => x.Id == existing.Id)
|
||||
.Set(x => x.VideosDir, request.VideosDir ?? existing.VideosDir)
|
||||
.Set(x => x.ImagesDir, request.ImagesDir ?? existing.ImagesDir)
|
||||
.Set(x => x.LabelsDir, request.LabelsDir ?? existing.LabelsDir)
|
||||
.Set(x => x.ResultsDir, request.ResultsDir ?? existing.ResultsDir)
|
||||
.Set(x => x.ThumbnailsDir, request.ThumbnailsDir ?? existing.ThumbnailsDir)
|
||||
.Set(x => x.GpsSatDir, request.GpsSatDir ?? existing.GpsSatDir)
|
||||
.Set(x => x.GpsRouteDir, request.GpsRouteDir ?? existing.GpsRouteDir)
|
||||
.UpdateAsync();
|
||||
pathResolver.Reset();
|
||||
}
|
||||
|
||||
public async Task<UserSettings?> GetUserSettings(Guid userId)
|
||||
{
|
||||
return await db.UserSettings.FirstOrDefaultAsync(x => x.UserId == userId);
|
||||
}
|
||||
|
||||
public async Task UpdateUserSettings(Guid userId, UpdateUserSettingsRequest request)
|
||||
{
|
||||
var existing = await db.UserSettings.FirstOrDefaultAsync(x => x.UserId == userId);
|
||||
if (existing == null)
|
||||
{
|
||||
await db.InsertAsync(new UserSettings
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
SelectedFlightId = request.SelectedFlightId,
|
||||
AnnotationsLeftPanelWidth = request.AnnotationsLeftPanelWidth,
|
||||
AnnotationsRightPanelWidth = request.AnnotationsRightPanelWidth,
|
||||
DatasetLeftPanelWidth = request.DatasetLeftPanelWidth,
|
||||
DatasetRightPanelWidth = request.DatasetRightPanelWidth
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.UserSettings
|
||||
.Where(x => x.Id == existing.Id)
|
||||
.Set(x => x.SelectedFlightId, request.SelectedFlightId ?? existing.SelectedFlightId)
|
||||
.Set(x => x.AnnotationsLeftPanelWidth, request.AnnotationsLeftPanelWidth ?? existing.AnnotationsLeftPanelWidth)
|
||||
.Set(x => x.AnnotationsRightPanelWidth, request.AnnotationsRightPanelWidth ?? existing.AnnotationsRightPanelWidth)
|
||||
.Set(x => x.DatasetLeftPanelWidth, request.DatasetLeftPanelWidth ?? existing.DatasetLeftPanelWidth)
|
||||
.Set(x => x.DatasetRightPanelWidth, request.DatasetRightPanelWidth ?? existing.DatasetRightPanelWidth)
|
||||
.UpdateAsync();
|
||||
}
|
||||
|
||||
public async Task<CameraSettings?> GetCameraSettings()
|
||||
{
|
||||
return await db.CameraSettings.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateCameraSettings(UpdateCameraSettingsRequest request)
|
||||
{
|
||||
var existing = await db.CameraSettings.FirstOrDefaultAsync();
|
||||
if (existing == null)
|
||||
{
|
||||
await db.InsertAsync(new CameraSettings
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Altitude = request.Altitude ?? 100,
|
||||
FocalLength = request.FocalLength ?? 50,
|
||||
SensorWidth = request.SensorWidth ?? 36
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.CameraSettings
|
||||
.Where(x => x.Id == existing.Id)
|
||||
.Set(x => x.Altitude, request.Altitude ?? existing.Altitude)
|
||||
.Set(x => x.FocalLength, request.FocalLength ?? existing.FocalLength)
|
||||
.Set(x => x.SensorWidth, request.SensorWidth ?? existing.SensorWidth)
|
||||
.UpdateAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Azaion.Annotations.Services;
|
||||
|
||||
public class TokenService
|
||||
{
|
||||
private readonly string _jwtSecret;
|
||||
private readonly double _accessTokenHours;
|
||||
|
||||
public TokenService(string jwtSecret, double accessTokenHours = 4)
|
||||
{
|
||||
_jwtSecret = jwtSecret;
|
||||
_accessTokenHours = accessTokenHours;
|
||||
}
|
||||
|
||||
public string? RefreshAccessToken(string refreshToken)
|
||||
{
|
||||
var principal = ValidateToken(refreshToken);
|
||||
if (principal == null)
|
||||
return null;
|
||||
|
||||
var tokenType = principal.FindFirstValue("token_type");
|
||||
if (tokenType != "refresh")
|
||||
return null;
|
||||
|
||||
var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var email = principal.FindFirstValue(ClaimTypes.Name);
|
||||
var role = principal.FindFirstValue(ClaimTypes.Role);
|
||||
|
||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(email))
|
||||
return null;
|
||||
|
||||
return CreateAccessToken(userId, email, role);
|
||||
}
|
||||
|
||||
private string CreateAccessToken(string userId, string email, string? role)
|
||||
{
|
||||
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecret));
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, userId),
|
||||
new(ClaimTypes.Name, email),
|
||||
new("token_type", "access")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(role))
|
||||
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Expires = DateTime.UtcNow.AddHours(_accessTokenHours),
|
||||
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
|
||||
private ClaimsPrincipal? ValidateToken(string token)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var validationParams = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecret)),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
return tokenHandler.ValidateToken(token, validationParams, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user