mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
- VendorAccessToken model + unique-index migration; TokenLifetimeDays config - VendorPortalTokenService: CSPRNG token generation, rotation, revocation - VendorPortalController: public portal API guarded by X-Vendor-Token header; dispatches list/detail, accept, vendor status transitions, cancel request, checklist updates, signoffs (vendor + customer), comments with dispatcher attribution via AspNetUsers join - VendorController: portal-token admin endpoints (get / rotate / revoke) - WorkOrderController: dispatch email now uses vendor portal URL and HTML-encodes user fields; AddDispatchComment now stores CommentType='dispatcher' with the SHOC user's name so portal can attribute the author - DispatchPublicController: deprecated per-dispatch GET accept flow returns a static 'link no longer active' page (no state mutation)
92 lines
3.2 KiB
C#
92 lines
3.2 KiB
C#
using System.Security.Cryptography;
|
|
using Data.SeaHavenIndustries;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Api.SeaHavenIndustries.Helper
|
|
{
|
|
public class VendorPortalTokenService
|
|
{
|
|
private readonly ApplicationDbContext _db;
|
|
private readonly int _lifetimeDays;
|
|
|
|
public VendorPortalTokenService(ApplicationDbContext db, IConfiguration config)
|
|
{
|
|
_db = db;
|
|
_lifetimeDays = config.GetValue<int?>("VendorPortal:TokenLifetimeDays") ?? 365;
|
|
}
|
|
|
|
public async Task<VendorAccessToken> GetOrCreateActiveTokenAsync(int vendorId)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var existing = await _db.VendorAccessTokens
|
|
.Where(t => t.VendorId == vendorId
|
|
&& t.RevokedAt == null
|
|
&& t.ExpiresAt > now
|
|
&& (t.IsDeleted == null || t.IsDeleted == false))
|
|
.OrderByDescending(t => t.IssuedAt)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (existing != null) return existing;
|
|
|
|
var token = new VendorAccessToken
|
|
{
|
|
VendorId = vendorId,
|
|
Token = GenerateToken(),
|
|
IssuedAt = now,
|
|
ExpiresAt = now.AddDays(_lifetimeDays),
|
|
CreatedDate = now
|
|
};
|
|
_db.VendorAccessTokens.Add(token);
|
|
await _db.SaveChangesAsync();
|
|
return token;
|
|
}
|
|
|
|
public async Task<Vendor?> ResolveVendorAsync(string? token)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token)) return null;
|
|
var now = DateTime.UtcNow;
|
|
var record = await _db.VendorAccessTokens
|
|
.Include(t => t.Vendor)
|
|
.FirstOrDefaultAsync(t => t.Token == token
|
|
&& t.RevokedAt == null
|
|
&& t.ExpiresAt > now
|
|
&& (t.IsDeleted == null || t.IsDeleted == false));
|
|
|
|
if (record?.Vendor == null) return null;
|
|
|
|
record.LastUsedAt = now;
|
|
await _db.SaveChangesAsync();
|
|
return record.Vendor;
|
|
}
|
|
|
|
public async Task<VendorAccessToken> RotateAsync(int vendorId)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var active = await _db.VendorAccessTokens
|
|
.Where(t => t.VendorId == vendorId && t.RevokedAt == null)
|
|
.ToListAsync();
|
|
foreach (var t in active) t.RevokedAt = now;
|
|
await _db.SaveChangesAsync();
|
|
return await GetOrCreateActiveTokenAsync(vendorId);
|
|
}
|
|
|
|
public async Task RevokeAllAsync(int vendorId)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var active = await _db.VendorAccessTokens
|
|
.Where(t => t.VendorId == vendorId && t.RevokedAt == null)
|
|
.ToListAsync();
|
|
foreach (var t in active) t.RevokedAt = now;
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private static string GenerateToken()
|
|
{
|
|
var bytes = RandomNumberGenerator.GetBytes(32);
|
|
return Convert.ToBase64String(bytes)
|
|
.Replace("+", "-")
|
|
.Replace("/", "_")
|
|
.TrimEnd('=');
|
|
}
|
|
}
|
|
}
|