using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using TeamUp.Modules.Integrations.Domain; using TeamUp.Modules.Integrations.Persistence; using TeamUp.Modules.Integrations.Security; using TeamUp.SharedKernel.Access; using TeamUp.SharedKernel.Ai; using TeamUp.SharedKernel.Modularity; namespace TeamUp.Modules.Integrations.Endpoints; internal static class IntegrationsEndpoints { public static void Map(IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("/api/integrations").WithTags("Integrations"); group.MapGet("/ping", () => TypedResults.Ok(new ModulePing("integrations"))); group.MapPost("/api-configs", CreateApiConfig).RequireAuthorization(); group.MapGet("/api-configs", ListApiConfigs).RequireAuthorization(); group.MapPost("/api-configs/{id:guid}/test", TestApiConfig).RequireAuthorization(); group.MapDelete("/api-configs/{id:guid}", DeleteApiConfig).RequireAuthorization(); } // Owner-only. Encrypts the key; the response never includes it. private static async Task CreateApiConfig( CreateApiConfigRequest request, ICurrentUser user, IPermissionService permissions, IntegrationsDbContext db, ISecretProtector protector, TimeProvider clock, CancellationToken ct) { if (!permissions.Has(Capability.ManageApiKeys, ScopeRef.Org(request.OrganizationId))) { return Results.Forbid(); } if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Provider) || string.IsNullOrWhiteSpace(request.Model) || string.IsNullOrWhiteSpace(request.ApiKey)) { return Results.BadRequest("Name, provider, model and apiKey are required."); } var config = new ApiConfig( request.OrganizationId, request.Name.Trim(), request.Provider.Trim(), request.Model.Trim(), request.Endpoint, protector.Protect(request.ApiKey), user.MemberId, clock.GetUtcNow()); db.ApiConfigs.Add(config); await db.SaveChangesAsync(ct); return Results.Ok(ToDto(config)); } // Team owners may list (to assign) — without ever seeing the key. private static async Task ListApiConfigs( Guid organizationId, IPermissionService permissions, IntegrationsDbContext db, CancellationToken ct) { if (!permissions.Has(Capability.ConfigureAgents, ScopeRef.Org(organizationId))) { return Results.Forbid(); } var configs = await db.ApiConfigs .Where(c => c.OrganizationId == organizationId) .OrderBy(c => c.Name) .Select(c => new ApiConfigDto(c.Id, c.Name, c.Provider, c.Model, c.Endpoint)) .ToListAsync(ct); return Results.Ok(configs); } // Owner-only. Resolves + decrypts server-side, makes a tiny model call, returns the outcome. private static async Task TestApiConfig( Guid id, IPermissionService permissions, IntegrationsDbContext db, IApiConfigResolver resolver, IModelClient model, CancellationToken ct) { var config = await db.ApiConfigs.FirstOrDefaultAsync(c => c.Id == id, ct); if (config is null) { return Results.NotFound(); } if (!permissions.Has(Capability.ManageApiKeys, ScopeRef.Org(config.OrganizationId))) { return Results.Forbid(); } var resolved = await resolver.ResolveAsync(id, ct); if (resolved is null) { return Results.NotFound(); } var completion = await model.CompleteAsync( new ModelRequest(resolved.Provider, resolved.Model, resolved.ApiKey, resolved.Endpoint, "ping", MaxTokens: 16), ct); var sample = completion.Text is { Length: > 0 } text ? text[..Math.Min(text.Length, 80)] : null; return Results.Ok(new TestResultDto(completion.Success, completion.Error, completion.LatencyMs, sample)); } private static async Task DeleteApiConfig( Guid id, IPermissionService permissions, IntegrationsDbContext db, CancellationToken ct) { var config = await db.ApiConfigs.FirstOrDefaultAsync(c => c.Id == id, ct); if (config is null) { return Results.NotFound(); } if (!permissions.Has(Capability.ManageApiKeys, ScopeRef.Org(config.OrganizationId))) { return Results.Forbid(); } db.ApiConfigs.Remove(config); await db.SaveChangesAsync(ct); return Results.NoContent(); } private static ApiConfigDto ToDto(ApiConfig config) => new(config.Id, config.Name, config.Provider, config.Model, config.Endpoint); }