85 lines
2.8 KiB
C#
85 lines
2.8 KiB
C#
using System.Security.Cryptography;
|
|
using backend.Data;
|
|
using backend.Devices;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace backend.Pairing;
|
|
|
|
public record PairResult(Guid DeviceId, string DeviceToken);
|
|
|
|
public class PairingService
|
|
{
|
|
private const string Alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O, 0, I, 1, L
|
|
private const int CodeLength = 8;
|
|
public static readonly TimeSpan CodeTtl = TimeSpan.FromMinutes(10);
|
|
|
|
private readonly AppDbContext? _db;
|
|
private readonly DeviceTokenService? _tokens;
|
|
|
|
public PairingService() { }
|
|
|
|
public PairingService(AppDbContext db, DeviceTokenService tokens)
|
|
{
|
|
_db = db;
|
|
_tokens = tokens;
|
|
}
|
|
|
|
public string GenerateCode()
|
|
{
|
|
Span<byte> bytes = stackalloc byte[CodeLength];
|
|
RandomNumberGenerator.Fill(bytes);
|
|
Span<char> chars = stackalloc char[CodeLength];
|
|
for (var i = 0; i < CodeLength; i++)
|
|
chars[i] = Alphabet[bytes[i] % Alphabet.Length];
|
|
return new string(chars);
|
|
}
|
|
|
|
public async Task<PairingCode> IssueAsync(Guid ownerUserId, string deviceName, CancellationToken ct)
|
|
{
|
|
if (_db is null) throw new InvalidOperationException("PairingService not constructed with DB context.");
|
|
var entity = new PairingCode
|
|
{
|
|
Code = GenerateCode(),
|
|
OwnerUserId = ownerUserId,
|
|
DeviceName = deviceName,
|
|
ExpiresAt = DateTimeOffset.UtcNow.Add(CodeTtl),
|
|
};
|
|
_db.PairingCodes.Add(entity);
|
|
await _db.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<PairResult?> ConsumeAsync(string code, CancellationToken ct)
|
|
{
|
|
if (_db is null || _tokens is null)
|
|
throw new InvalidOperationException("PairingService not constructed with DB context + token service.");
|
|
var now = DateTimeOffset.UtcNow;
|
|
var pc = await _db.PairingCodes.FirstOrDefaultAsync(p => p.Code == code, ct);
|
|
if (pc is null || pc.ConsumedAt is not null || pc.ExpiresAt < now)
|
|
return null;
|
|
|
|
var (plaintext, hash) = _tokens.Generate();
|
|
var defaults = await _db.SystemSettings.FirstAsync(ct);
|
|
|
|
var device = new Device
|
|
{
|
|
OwnerUserId = pc.OwnerUserId,
|
|
Name = pc.DeviceName,
|
|
PairedAt = now,
|
|
TokenHash = hash,
|
|
};
|
|
device.Config = new DeviceConfig
|
|
{
|
|
DeviceId = device.Id,
|
|
SystemPrompt = defaults.DefaultSystemPrompt,
|
|
Voice = defaults.DefaultVoice,
|
|
Model = defaults.DefaultModel,
|
|
IdleTimeoutSeconds = defaults.DefaultIdleTimeoutSeconds,
|
|
};
|
|
_db.Devices.Add(device);
|
|
pc.ConsumedAt = now;
|
|
await _db.SaveChangesAsync(ct);
|
|
return new PairResult(device.Id, plaintext);
|
|
}
|
|
}
|