121 lines
4.7 KiB
C#
121 lines
4.7 KiB
C#
|
|
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<IResult> 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<IResult> 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<IResult> 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<IResult> 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);
|
||
|
|
}
|