Skip to content

Agent 3: Portal ASP.NET Core backend (src/portal/) #3

@TomProkop

Description

@TomProkop

Objective

Create the AgentBox Portal — an ASP.NET Core 10 application that serves as the dashboard, REST API, and YARP reverse proxy. All in a single deployment.

Scope

Files to create (ALL in src/portal/ — do NOT touch any other directory):

Project Structure

src/portal/
├── AgentBox.Portal.sln
├── AgentBox.Portal/
│   ├── AgentBox.Portal.csproj
│   ├── Program.cs
│   ├── appsettings.json
│   ├── appsettings.Development.json
│   ├── Dockerfile
│   ├── Controllers/
│   │   ├── BoxesController.cs       — CRUD for AgentBox containers
│   │   ├── AuthController.cs        — OAuth callbacks (GitHub, ADO, Atlassian)
│   │   └── TokenController.cs       — Token status, refresh, shared Copilot token
│   ├── Services/
│   │   ├── AciService.cs            — Azure SDK: create/list/delete ACI containers
│   │   ├── BoxMetadataService.cs    — Azure Table Storage: track ownership, tokens, grants
│   │   ├── GitHubOAuthService.cs    — GitHub App OAuth flow + Copilot license check
│   │   ├── AdoOAuthService.cs       — Azure DevOps OAuth flow
│   │   ├── AtlassianOAuthService.cs — Atlassian 3LO OAuth flow
│   │   ├── SharePointService.cs     — Site discovery + Sites.Selected grant/revoke
│   │   ├── DataverseService.cs      — MI app user provisioning per environment
│   │   ├── SshCertificateService.cs — Key Vault SSH cert signing
│   │   └── YarpConfigService.cs     — Dynamic YARP route management
│   ├── Middleware/
│   │   └── BoxAuthorizationMiddleware.cs — Owner/shared/admin access checks
│   ├── Models/
│   │   ├── AgentBoxInstance.cs      — Container state model
│   │   ├── SpawnRequest.cs          — Spawn form input model
│   │   └── BoxMetadata.cs           — Table Storage entity
│   └── ClientApp/                   — PLACEHOLDER ONLY (React created by Agent 5)
│       └── .gitkeep
└── AgentBox.Portal.Tests/
    └── AgentBox.Portal.Tests.csproj

Key NuGet Packages

  • Yarp.ReverseProxy — Microsoft's reverse proxy
  • Microsoft.Identity.Web — Entra ID auth
  • Azure.ResourceManager.ContainerInstance — ACI management
  • Azure.Data.Tables — Table Storage for BoxMetadata
  • Azure.Security.KeyVault.Secrets + Azure.Security.KeyVault.Certificates — Key Vault access
  • Microsoft.Graph — SharePoint Sites.Selected + site discovery
  • Azure.Identity — DefaultAzureCredential for managed identity

Program.cs Key Configuration

  • builder.Services.AddReverseProxy() with in-memory config provider (YarpConfigService)
  • builder.Services.AddMicrosoftIdentityWebApiAuthentication() — Entra ID bearer token validation
  • Table Storage client DI registration
  • All 9 services registered as scoped/singleton as appropriate
  • app.UseStaticFiles() — serves React build from wwwroot/
  • app.MapReverseProxy() — YARP catches wildcard subdomains
  • app.MapControllers() — API endpoints
  • Fallback to index.html for SPA routing

API Endpoints

  • POST /api/boxes — spawn (accepts SpawnRequest with name, selected services, environments, sites)
  • GET /api/boxes — list user's boxes
  • GET /api/boxes/{name} — get box details
  • DELETE /api/boxes/{name} — destroy box + cleanup (Dataverse app users, SharePoint grants)
  • GET /api/boxes/{name}/status — health check
  • POST /api/boxes/{name}/ssh-certificate — sign SSH public key
  • GET /auth/github/callback — GitHub OAuth callback
  • GET /auth/ado/callback — ADO OAuth callback
  • GET /auth/atlassian/callback — Atlassian OAuth callback

YARP Dynamic Routing

  • YarpConfigService maintains in-memory route config
  • When box is spawned: add route {boxName}.agentbox.networg.comhttp://{aciIp}:80
  • When box is destroyed: remove route
  • On startup: load active boxes from Table Storage and rebuild routes

BoxMetadata (Azure Table Storage)

  • PartitionKey: userId (Entra ID object ID)
  • RowKey: boxName
  • Fields: aciResourceId, fqdn, createdAt, expiresAt (5-day TTL), ghToken (encrypted), adoToken, jsmToken, spSites (JSON), dvEnvironments (JSON), sshCertPermissions

Copilot License Check (in GitHubOAuthService)

  • After GitHub OAuth: resolve username from token
  • GET /orgs/NETWORG/members/{username}/copilot → 200 (has seat) or 404 (no seat)
  • If no seat: fetch shared Copilot token from Key Vault (secret: shared-copilot-refresh-token)
  • Mint fresh 8hr access token from refresh token, inject as COPILOT_GITHUB_TOKEN
  • If has seat: use user's own ghu_* for both GH_TOKEN and COPILOT_GITHUB_TOKEN

Key Design Decisions

  • React SPA served as static files from wwwroot/ (built by Agent 5, copied during Docker build)
  • Azure Table Storage for BoxMetadata (~$0.01/mo)
  • Cloudflare handles TLS at edge — portal runs on HTTP internally
  • Short-lived OAuth tokens (8hr) for shared Copilot account — portal refreshes server-side
  • Per-container MI preferred for SharePoint — test if Site Permissions API accepts MI appId

Conflict Prevention

This agent works ONLY in src/portal/. Creates ClientApp/.gitkeep as placeholder — Agent 5 fills it.
No other agent touches src/portal/.

Acceptance Criteria

  • dotnet build succeeds
  • dotnet test succeeds (at least one placeholder test)
  • All 9 services have reasonable implementations (can use TODOs for external API calls)
  • YARP configured with dynamic in-memory provider
  • Portal Dockerfile builds successfully
  • appsettings.json has all required configuration sections

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions