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 bytes = stackalloc byte[CodeLength]; RandomNumberGenerator.Fill(bytes); Span 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 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 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); } }