Forge
csharp4405de34
1using IntercomService.Contracts;
2using IntercomService.Data;
3using Microsoft.EntityFrameworkCore;
4
5namespace IntercomService.Services;
6
7public sealed class AgentAccountService(IntercomDbContext db)
8{
9 public async Task<AgentDto?> ProvisionAsync(
10 string teamId,
11 string displayName,
12 string? avatarGlyph,
13 CancellationToken ct)
14 {
15 var memberId = $"agent-{Guid.NewGuid():N}";
16 var member = new MemberEntity
17 {
18 MemberId = memberId,
19 Issuer = "agent",
20 Subject = memberId,
21 DisplayName = displayName.Trim(),
22 MemberKind = MemberKinds.Agent,
23 AvatarGlyph = avatarGlyph,
24 CreatedAtUtc = DateTimeOffset.UtcNow,
25 };
26
27 db.Members.Add(member);
28 db.TeamMembers.Add(new TeamMemberEntity
29 {
30 TeamId = teamId,
31 MemberId = memberId,
32 TeamRole = TeamRoles.Agent,
33 JoinedAtUtc = DateTimeOffset.UtcNow,
34 });
35
36 var plain = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(48));
37 db.AgentCredentials.Add(new AgentCredentialEntity
38 {
39 CredentialId = Guid.NewGuid().ToString("N"),
40 MemberId = memberId,
41 TokenHash = JwtTokenService.HashToken(plain),
42 CreatedAtUtc = DateTimeOffset.UtcNow,
43 });
44
45 await db.SaveChangesAsync(ct).ConfigureAwait(false);
46 return new AgentDto(memberId, member.DisplayName, avatarGlyph, plain);
47 }
48
49 public async Task<bool> PatchAsync(
50 string teamId,
51 string memberId,
52 PatchAgentRequest body,
53 bool isAdmin,
54 CancellationToken ct)
55 {
56 var membership = await db.TeamMembers
57 .Include(x => x.Member)
58 .FirstOrDefaultAsync(x => x.TeamId == teamId && x.MemberId == memberId, ct)
59 .ConfigureAwait(false);
60
61 if (membership?.Member is null || !string.Equals(membership.Member.MemberKind, MemberKinds.Agent, StringComparison.Ordinal))
62 return false;
63
64 if (!isAdmin && !string.Equals(membership.MemberId, memberId, StringComparison.Ordinal))
65 return false;
66
67 if (!string.IsNullOrWhiteSpace(body.DisplayName))
68 membership.Member.DisplayName = body.DisplayName.Trim();
69
70 if (body.AvatarGlyph is not null)
71 membership.Member.AvatarGlyph = string.IsNullOrWhiteSpace(body.AvatarGlyph) ? null : body.AvatarGlyph.Trim();
72
73 if (body.RevokeCredentials)
74 {
75 var creds = await db.AgentCredentials
76 .Where(x => x.MemberId == memberId && x.RevokedAtUtc == null)
77 .ToListAsync(ct)
78 .ConfigureAwait(false);
79 foreach (var c in creds)
80 c.RevokedAtUtc = DateTimeOffset.UtcNow;
81 }
82
83 await db.SaveChangesAsync(ct).ConfigureAwait(false);
84 return true;
85 }
86
87 public async Task<MemberEntity?> AuthenticateAgentCredentialAsync(string plainToken, CancellationToken ct)
88 {
89 var hash = JwtTokenService.HashToken(plainToken);
90 var cred = await db.AgentCredentials
91 .Include(x => x.Member)
92 .FirstOrDefaultAsync(x => x.TokenHash == hash && x.RevokedAtUtc == null, ct)
93 .ConfigureAwait(false);
94
95 if (cred?.Member is null)
96 return null;
97
98 if (cred.ExpiresAtUtc is not null && cred.ExpiresAtUtc < DateTimeOffset.UtcNow)
99 return null;
100
101 return cred.Member;
102 }
103}
104
View only · write via MCP/CIDE