using Fleet.Core; namespace Monitor.Backend.Application; public sealed record CreateJoinTokenRequest(string? Note, IReadOnlyList? Labels, bool AutoApprove, int MaxUses, int ExpiresInHours); public sealed record CreatedJoinToken(JoinTokenInfo Info, string Plaintext, string InstallCommand); /// 加入令牌:明文只在创建时返回一次,服务端只保存 SHA-256。 public sealed class JoinTokenService { private readonly IJoinTokenStore _store; private readonly BackendOptions _options; private readonly TimeProvider _time; public JoinTokenService(IJoinTokenStore store, BackendOptions options, TimeProvider time) { _store = store; _options = options; _time = time; } public async ValueTask> CreateAsync(CreateJoinTokenRequest request, string? requestBaseUrl, CancellationToken ct) { var errors = new Dictionary(); if (request.MaxUses is < 1 or > 1000) { errors["max_uses"] = "must be between 1 and 1000"; } if (request.ExpiresInHours is < 1 or > 24 * 365) { errors["expires_in_hours"] = "must be between 1 and 8760"; } if (request.Labels is { } labels && (labels.Count > 16 || labels.Any(l => l.Length is 0 or > 32))) { errors["labels"] = "at most 16 labels of 1-32 characters"; } if (request.Note is { Length: > 200 }) { errors["note"] = "must be at most 200 characters"; } if (errors.Count > 0) { return ValidationError.Fail(errors); } string plaintext = TokenHash.NewToken("jt"); long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); JoinTokenInfo info = await _store.CreateAsync( Ulid.NewPrefixed("jtk", _time), TokenHash.Sha256(plaintext), request.Note, request.Labels ?? [], request.AutoApprove, request.MaxUses, now + (request.ExpiresInHours * 3_600_000L), now, ct); string baseUrl = (_options.PublicUrl ?? requestBaseUrl ?? "https://panel.example.com").TrimEnd('/'); string command = $"curl -fsSL {baseUrl}/install.sh | sh -s -- --join {plaintext} --name "; return Result.Ok(new CreatedJoinToken(info, plaintext, command)); } public ValueTask> ListAsync(CancellationToken ct) => _store.ListAsync(ct); public ValueTask RevokeAsync(string id, CancellationToken ct) => _store.RevokeAsync(id, _time.GetUtcNow().ToUnixTimeMilliseconds(), ct); }