@agenticmail/enterprise 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- package/tsconfig.json +14 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# AgenticMail Enterprise Architecture
|
|
2
|
+
|
|
3
|
+
## Vision
|
|
4
|
+
Company installs `npx agenticmail-enterprise` ā runs setup wizard ā gets a cloud-hosted admin dashboard URL ā manages AI agent identities, email, auth, compliance from that dashboard.
|
|
5
|
+
|
|
6
|
+
## Core Principles
|
|
7
|
+
1. **Cloud-first**: No local servers. Deploys to user's cloud or our managed infra.
|
|
8
|
+
2. **Bring your own database**: Support Postgres, MySQL, SQLite, MongoDB, DynamoDB, CockroachDB, PlanetScale, Turso, Supabase, Neon ā anything.
|
|
9
|
+
3. **Bring your own cloud**: Deploy to Fly.io, Railway, Render, AWS, GCP, Azure, Vercel, or managed by us.
|
|
10
|
+
4. **Auto-provisioned URL**: Instant `<company>.agenticmail.cloud` subdomain, with custom domain support.
|
|
11
|
+
5. **Admin UI**: Web dashboard for agent CRUD, audit logs, rules, compliance.
|
|
12
|
+
|
|
13
|
+
## User Journey
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
$ npx agenticmail-enterprise
|
|
17
|
+
|
|
18
|
+
š¢ AgenticMail Enterprise Setup
|
|
19
|
+
|
|
20
|
+
? Company name: Acme Corp
|
|
21
|
+
? Admin email: admin@acme.com
|
|
22
|
+
? Database: (choose one)
|
|
23
|
+
āø PostgreSQL (connection string)
|
|
24
|
+
MySQL (connection string)
|
|
25
|
+
MongoDB (connection string)
|
|
26
|
+
SQLite (embedded)
|
|
27
|
+
Turso (LibSQL)
|
|
28
|
+
PlanetScale (MySQL-compatible)
|
|
29
|
+
Supabase (Postgres)
|
|
30
|
+
Neon (Postgres)
|
|
31
|
+
DynamoDB (AWS)
|
|
32
|
+
CockroachDB
|
|
33
|
+
|
|
34
|
+
? Database connection: postgresql://...
|
|
35
|
+
? Deploy to: (choose one)
|
|
36
|
+
āø AgenticMail Cloud (managed, free tier)
|
|
37
|
+
Fly.io
|
|
38
|
+
Railway
|
|
39
|
+
Render
|
|
40
|
+
Docker (self-hosted)
|
|
41
|
+
|
|
42
|
+
? Custom domain (optional): mail.acme.com
|
|
43
|
+
|
|
44
|
+
ā³ Provisioning...
|
|
45
|
+
ā Database schema created
|
|
46
|
+
ā Admin account created
|
|
47
|
+
ā DKIM/SPF/DMARC configured
|
|
48
|
+
ā Deployed to agenticmail.cloud
|
|
49
|
+
|
|
50
|
+
š Your dashboard is live!
|
|
51
|
+
URL: https://acme.agenticmail.cloud
|
|
52
|
+
Admin: admin@acme.com (check email for password)
|
|
53
|
+
|
|
54
|
+
Add custom domain later:
|
|
55
|
+
CNAME mail.acme.com ā acme.agenticmail.cloud
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Package Structure
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
@agenticmail/enterprise
|
|
62
|
+
āāā src/
|
|
63
|
+
ā āāā index.ts # CLI entry point
|
|
64
|
+
ā āāā setup/
|
|
65
|
+
ā ā āāā wizard.ts # Interactive setup flow
|
|
66
|
+
ā ā āāā database.ts # DB adapter factory
|
|
67
|
+
ā ā āāā deploy.ts # Cloud deployment orchestrator
|
|
68
|
+
ā āāā db/
|
|
69
|
+
ā ā āāā adapter.ts # Abstract DB interface
|
|
70
|
+
ā ā āāā postgres.ts # PostgreSQL adapter
|
|
71
|
+
ā ā āāā mysql.ts # MySQL adapter
|
|
72
|
+
ā ā āāā mongodb.ts # MongoDB adapter
|
|
73
|
+
ā ā āāā sqlite.ts # SQLite adapter (dev/small teams)
|
|
74
|
+
ā ā āāā turso.ts # Turso/LibSQL adapter
|
|
75
|
+
ā ā āāā dynamodb.ts # DynamoDB adapter
|
|
76
|
+
ā ā āāā migrations/ # Schema migrations (per adapter)
|
|
77
|
+
ā āāā auth/
|
|
78
|
+
ā ā āāā saml.ts # SAML 2.0 SP
|
|
79
|
+
ā ā āāā oidc.ts # OAuth 2.0 / OpenID Connect
|
|
80
|
+
ā ā āāā scim.ts # SCIM provisioning
|
|
81
|
+
ā ā āāā api-keys.ts # API key management
|
|
82
|
+
ā ā āāā sessions.ts # Session management
|
|
83
|
+
ā āāā admin/
|
|
84
|
+
ā ā āāā dashboard.ts # Admin API routes
|
|
85
|
+
ā ā āāā agents.ts # Agent CRUD
|
|
86
|
+
ā ā āāā audit.ts # Audit log viewer
|
|
87
|
+
ā ā āāā rules.ts # Email rules management
|
|
88
|
+
ā ā āāā compliance.ts # DLP, retention policies
|
|
89
|
+
ā ā āāā billing.ts # Usage tracking, plans
|
|
90
|
+
ā āāā deploy/
|
|
91
|
+
ā ā āāā fly.ts # Fly.io deployment
|
|
92
|
+
ā ā āāā railway.ts # Railway deployment
|
|
93
|
+
ā ā āāā render.ts # Render deployment
|
|
94
|
+
ā ā āāā docker.ts # Docker/self-hosted
|
|
95
|
+
ā ā āāā managed.ts # AgenticMail Cloud (our infra)
|
|
96
|
+
ā āāā ui/ # Admin dashboard (React/Next.js)
|
|
97
|
+
ā ā āāā app/
|
|
98
|
+
ā ā ā āāā layout.tsx
|
|
99
|
+
ā ā ā āāā page.tsx # Dashboard home
|
|
100
|
+
ā ā ā āāā agents/ # Agent management
|
|
101
|
+
ā ā ā āāā audit/ # Audit logs
|
|
102
|
+
ā ā ā āāā settings/ # Company settings
|
|
103
|
+
ā ā ā āāā auth/ # SSO config
|
|
104
|
+
ā ā ā āāā compliance/ # DLP, retention
|
|
105
|
+
ā ā āāā components/
|
|
106
|
+
ā āāā server.ts # Express/Hono server (API + UI)
|
|
107
|
+
āāā package.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Database Adapter Interface
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
interface DatabaseAdapter {
|
|
114
|
+
// Connection
|
|
115
|
+
connect(config: DatabaseConfig): Promise<void>;
|
|
116
|
+
disconnect(): Promise<void>;
|
|
117
|
+
migrate(): Promise<void>;
|
|
118
|
+
|
|
119
|
+
// Agents
|
|
120
|
+
createAgent(agent: AgentInput): Promise<Agent>;
|
|
121
|
+
getAgent(id: string): Promise<Agent | null>;
|
|
122
|
+
listAgents(filters?: AgentFilters): Promise<Agent[]>;
|
|
123
|
+
updateAgent(id: string, updates: Partial<Agent>): Promise<Agent>;
|
|
124
|
+
deleteAgent(id: string): Promise<void>;
|
|
125
|
+
archiveAgent(id: string): Promise<DeletionReport>;
|
|
126
|
+
|
|
127
|
+
// Email
|
|
128
|
+
storeEmail(email: StoredEmail): Promise<void>;
|
|
129
|
+
getEmail(uid: number): Promise<StoredEmail | null>;
|
|
130
|
+
searchEmails(query: SearchQuery): Promise<StoredEmail[]>;
|
|
131
|
+
|
|
132
|
+
// Audit
|
|
133
|
+
logEvent(event: AuditEvent): Promise<void>;
|
|
134
|
+
queryAuditLog(filters: AuditFilters): Promise<AuditEvent[]>;
|
|
135
|
+
|
|
136
|
+
// Auth
|
|
137
|
+
createUser(user: UserInput): Promise<User>;
|
|
138
|
+
getUser(id: string): Promise<User | null>;
|
|
139
|
+
getUserByEmail(email: string): Promise<User | null>;
|
|
140
|
+
|
|
141
|
+
// API Keys
|
|
142
|
+
createApiKey(key: ApiKeyInput): Promise<ApiKey>;
|
|
143
|
+
validateApiKey(key: string): Promise<ApiKey | null>;
|
|
144
|
+
revokeApiKey(id: string): Promise<void>;
|
|
145
|
+
|
|
146
|
+
// Rules & Compliance
|
|
147
|
+
createRule(rule: RuleInput): Promise<Rule>;
|
|
148
|
+
getRules(): Promise<Rule[]>;
|
|
149
|
+
getRetentionPolicy(): Promise<RetentionPolicy>;
|
|
150
|
+
setRetentionPolicy(policy: RetentionPolicy): Promise<void>;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Deployment Architecture
|
|
155
|
+
|
|
156
|
+
### AgenticMail Cloud (Managed)
|
|
157
|
+
- Fly.io multi-region (us-east, eu-west, ap-southeast)
|
|
158
|
+
- Customer gets `<company>.agenticmail.cloud` subdomain
|
|
159
|
+
- Wildcard TLS via Fly.io
|
|
160
|
+
- Shared Fly.io org, isolated apps per customer
|
|
161
|
+
- Customer can add custom domain (CNAME ā our subdomain)
|
|
162
|
+
|
|
163
|
+
### Self-Hosted
|
|
164
|
+
- Single Docker image: `docker run -p 3000:3000 agenticmail/enterprise`
|
|
165
|
+
- Or via CLI: `npx agenticmail-enterprise start`
|
|
166
|
+
- Env vars for DB connection, SMTP, domain
|
|
167
|
+
|
|
168
|
+
## Admin Dashboard Pages
|
|
169
|
+
|
|
170
|
+
1. **Dashboard** ā Overview: active agents, emails sent/received, health
|
|
171
|
+
2. **Agents** ā Create, edit, archive, delete. Role assignment. Email config per agent.
|
|
172
|
+
3. **Audit Log** ā Who did what, when. Filterable, exportable.
|
|
173
|
+
4. **Authentication** ā SAML/OIDC setup, user management, API keys
|
|
174
|
+
5. **Compliance** ā DLP rules, retention policies, outbound guards
|
|
175
|
+
6. **Email Rules** ā Server-side rules (auto-tag, auto-move, auto-reply)
|
|
176
|
+
7. **Settings** ā Company info, domain, SMTP config, billing
|
|
177
|
+
8. **Integrations** ā Slack, Teams, Discord notifications
|
|
178
|
+
|
|
179
|
+
## Pricing Tiers (Future)
|
|
180
|
+
- **Free**: 3 agents, 1K emails/mo, community support
|
|
181
|
+
- **Team**: 25 agents, 50K emails/mo, SSO, audit logs ā $49/mo
|
|
182
|
+
- **Enterprise**: Unlimited, SCIM, DLP, retention, SLA, dedicated support ā $299/mo
|
|
183
|
+
- **Self-Hosted**: Unlimited, your infra ā $99/mo license
|
|
Binary file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AgenticMail Enterprise Dashboards
|
|
2
|
+
|
|
3
|
+
**Admin dashboards in every popular language.** Pick the one that matches your stack ā or use the zero-code HTML version if you're not a developer.
|
|
4
|
+
|
|
5
|
+
All dashboards connect to the same AgenticMail Enterprise API. They're interchangeable.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## šÆ Quick Start by Language
|
|
10
|
+
|
|
11
|
+
| Language | File | Dependencies | Run Command |
|
|
12
|
+
|----------|------|-------------|-------------|
|
|
13
|
+
| **HTML** (zero-code) | `html/index.html` | None! | Just open in browser |
|
|
14
|
+
| **PHP** | `php/index.php` | PHP 7.4+ | `php -S localhost:8080 index.php` |
|
|
15
|
+
| **Python** | `python/app.py` | Flask, requests | `pip install flask requests && python app.py` |
|
|
16
|
+
| **Ruby** | `ruby/app.rb` | Sinatra | `gem install sinatra && ruby app.rb` |
|
|
17
|
+
| **Express.js** | `express/app.js` | Express | `npm install express express-session && node app.js` |
|
|
18
|
+
| **React** (built-in) | _Served at `/dashboard`_ | None (bundled) | Comes with the enterprise server |
|
|
19
|
+
|
|
20
|
+
| **Go** | `go/main.go` | None (stdlib only) | `go run main.go` |
|
|
21
|
+
| **Java** | `java/AgenticMailDashboard.java` | JDK 11+ | `javac AgenticMailDashboard.java && java AgenticMailDashboard` |
|
|
22
|
+
| **C# / .NET** | `dotnet/Program.cs` | .NET 8+ | `dotnet new web && dotnet run` |
|
|
23
|
+
|
|
24
|
+
### Coming Soon
|
|
25
|
+
| Language | Status |
|
|
26
|
+
|----------|--------|
|
|
27
|
+
| **Laravel** (PHP framework) | Planned |
|
|
28
|
+
| **Django** (Python framework) | Planned |
|
|
29
|
+
| **Rails** (Ruby framework) | Planned |
|
|
30
|
+
| **Svelte** | Planned |
|
|
31
|
+
| **Vue.js** | Planned |
|
|
32
|
+
| **Angular** | Planned |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## š Non-Coders: Start Here
|
|
37
|
+
|
|
38
|
+
**You don't need to know how to code.** Use the HTML dashboard:
|
|
39
|
+
|
|
40
|
+
1. Download `html/index.html`
|
|
41
|
+
2. Open it in any web browser (Chrome, Safari, Firefox, Edge)
|
|
42
|
+
3. Enter your AgenticMail Enterprise server URL when prompted
|
|
43
|
+
4. Login with your admin credentials
|
|
44
|
+
|
|
45
|
+
That's it. No installation, no terminal, no coding.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## š How It Works
|
|
50
|
+
|
|
51
|
+
Every dashboard talks to the **AgenticMail Enterprise REST API**:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Your Dashboard ā REST API ā AgenticMail Enterprise Server
|
|
55
|
+
(any language) (JSON) (your database)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The API endpoints:
|
|
59
|
+
- `POST /auth/login` ā Get JWT token
|
|
60
|
+
- `GET /api/stats` ā Dashboard stats
|
|
61
|
+
- `GET /api/agents` ā List agents
|
|
62
|
+
- `POST /api/agents` ā Create agent
|
|
63
|
+
- `GET /api/users` ā List users
|
|
64
|
+
- `GET /api/api-keys` ā List API keys
|
|
65
|
+
- `GET /api/audit` ā Audit log
|
|
66
|
+
- `GET /api/settings` ā Organization settings
|
|
67
|
+
- `GET /health` ā Health check
|
|
68
|
+
|
|
69
|
+
Full API docs at your server's `/health` endpoint.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## šØ All Dashboards Include
|
|
74
|
+
|
|
75
|
+
- **Dark theme** ā Easy on the eyes
|
|
76
|
+
- **Responsive** ā Works on mobile, tablet, desktop
|
|
77
|
+
- **Authentication** ā Login with email/password, JWT sessions
|
|
78
|
+
- **Dashboard** ā Stats overview with agent/user/event counts
|
|
79
|
+
- **Agents** ā Create, list, archive AI agents
|
|
80
|
+
- **Users** ā Manage team members with roles (owner/admin/member/viewer)
|
|
81
|
+
- **API Keys** ā Create, list, revoke programmatic access keys
|
|
82
|
+
- **Audit Log** ā Paginated event history
|
|
83
|
+
- **Settings** ā Organization name, domain, branding
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## š§ Configuration
|
|
88
|
+
|
|
89
|
+
All dashboards use one environment variable:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
AGENTICMAIL_URL=https://your-company.agenticmail.cloud
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or edit the `API_URL` variable at the top of each file.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## šļø Build Your Own
|
|
100
|
+
|
|
101
|
+
The API is REST + JSON. Build a dashboard in any language:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Login
|
|
105
|
+
curl -X POST https://your-server/auth/login \
|
|
106
|
+
-H "Content-Type: application/json" \
|
|
107
|
+
-d '{"email":"admin@acme.com","password":"..."}'
|
|
108
|
+
# Returns: { "token": "eyJ...", "user": {...} }
|
|
109
|
+
|
|
110
|
+
# Use the token
|
|
111
|
+
curl https://your-server/api/stats \
|
|
112
|
+
-H "Authorization: Bearer eyJ..."
|
|
113
|
+
# Returns: { "totalAgents": 5, "activeAgents": 3, ... }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## š License
|
|
119
|
+
|
|
120
|
+
MIT ā Use these dashboards however you want.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// AgenticMail Enterprise Dashboard ā .NET / C# Edition
|
|
2
|
+
//
|
|
3
|
+
// Uses .NET 8+ Minimal API. No MVC, no Razor, no extra NuGet packages.
|
|
4
|
+
//
|
|
5
|
+
// Setup:
|
|
6
|
+
// dotnet new web -n AgenticMailDashboard
|
|
7
|
+
// cp Program.cs AgenticMailDashboard/Program.cs
|
|
8
|
+
// cd AgenticMailDashboard && dotnet run
|
|
9
|
+
//
|
|
10
|
+
// Or create project in one shot:
|
|
11
|
+
// mkdir AgenticMailDashboard && cd AgenticMailDashboard
|
|
12
|
+
// dotnet new web
|
|
13
|
+
// # Replace Program.cs with this file
|
|
14
|
+
// dotnet run
|
|
15
|
+
|
|
16
|
+
using System.Net.Http.Json;
|
|
17
|
+
using System.Text.Json;
|
|
18
|
+
using System.Web;
|
|
19
|
+
|
|
20
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
21
|
+
builder.Services.AddDistributedMemoryCache();
|
|
22
|
+
builder.Services.AddSession(o => { o.IdleTimeout = TimeSpan.FromHours(24); o.Cookie.HttpOnly = true; });
|
|
23
|
+
builder.Services.AddHttpClient();
|
|
24
|
+
|
|
25
|
+
var app = builder.Build();
|
|
26
|
+
app.UseSession();
|
|
27
|
+
|
|
28
|
+
var API_URL = Environment.GetEnvironmentVariable("AGENTICMAIL_URL") ?? "http://localhost:3000";
|
|
29
|
+
var httpFactory = app.Services.GetRequiredService<IHttpClientFactory>();
|
|
30
|
+
|
|
31
|
+
// āāā API Client āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
32
|
+
|
|
33
|
+
async Task<JsonElement?> Api(HttpContext ctx, string path, string method = "GET", object? body = null)
|
|
34
|
+
{
|
|
35
|
+
var client = httpFactory.CreateClient();
|
|
36
|
+
client.Timeout = TimeSpan.FromSeconds(10);
|
|
37
|
+
var token = ctx.Session.GetString("token");
|
|
38
|
+
if (token != null) client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
|
|
39
|
+
|
|
40
|
+
HttpResponseMessage resp;
|
|
41
|
+
var uri = $"{API_URL}{path}";
|
|
42
|
+
switch (method)
|
|
43
|
+
{
|
|
44
|
+
case "POST": resp = await client.PostAsJsonAsync(uri, body ?? new { }); break;
|
|
45
|
+
case "PATCH": resp = await client.PatchAsJsonAsync(uri, body ?? new { }); break;
|
|
46
|
+
case "DELETE": resp = await client.DeleteAsync(uri); break;
|
|
47
|
+
default: resp = await client.GetAsync(uri); break;
|
|
48
|
+
}
|
|
49
|
+
var json = await resp.Content.ReadAsStringAsync();
|
|
50
|
+
try { return JsonDocument.Parse(json).RootElement; }
|
|
51
|
+
catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
string Esc(string? s) => HttpUtility.HtmlEncode(s ?? "");
|
|
55
|
+
string Str(JsonElement? el, string prop) => el?.TryGetProperty(prop, out var v) == true ? v.ToString() : "";
|
|
56
|
+
int Int(JsonElement? el, string prop) => el?.TryGetProperty(prop, out var v) == true && v.TryGetInt32(out var n) ? n : 0;
|
|
57
|
+
|
|
58
|
+
string Badge(string status)
|
|
59
|
+
{
|
|
60
|
+
var colors = new Dictionary<string, string>
|
|
61
|
+
{
|
|
62
|
+
["active"] = "#22c55e", ["archived"] = "#888", ["suspended"] = "#ef4444",
|
|
63
|
+
["owner"] = "#f59e0b", ["admin"] = "#6366f1", ["member"] = "#888", ["viewer"] = "#555"
|
|
64
|
+
};
|
|
65
|
+
var c = colors.GetValueOrDefault(status, "#888");
|
|
66
|
+
return $"<span style='display:inline-block;padding:2px 10px;border-radius:999px;font-size:11px;font-weight:600;background:{c}20;color:{c}'>{Esc(status)}</span>";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// āāā CSS & Layout āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
70
|
+
|
|
71
|
+
const string CSS = @"*{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e4e4ef;--dim:#8888a0;--muted:#55556a;--primary:#6366f1;--success:#22c55e;--danger:#ef4444;--warning:#f59e0b}body{font-family:-apple-system,sans-serif;background:var(--bg);color:var(--text)}.layout{display:flex;min-height:100vh}.sidebar{width:240px;background:var(--surface);border-right:1px solid var(--border);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column}.sh{padding:20px;border-bottom:1px solid var(--border)}.sh h2{font-size:16px}.sh h2 em{font-style:normal;color:var(--primary)}.sh small{font-size:11px;color:var(--muted);display:block;margin-top:2px}.nav{flex:1;padding:8px 0}.ns{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);padding:12px 20px 4px}.nav a{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--dim);text-decoration:none;font-size:13px}.nav a:hover{color:var(--text);background:rgba(255,255,255,0.03)}.nav a.on{color:var(--primary);background:rgba(99,102,241,0.12);border-right:2px solid var(--primary)}.sf{padding:16px 20px;border-top:1px solid var(--border);font-size:12px}.content{flex:1;margin-left:240px;padding:32px;max-width:1100px}h2.t{font-size:22px;font-weight:700;margin-bottom:4px}.desc{font-size:13px;color:var(--dim);margin-bottom:24px}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:24px}.stat{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px}.stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em}.stat .v{font-size:30px;font-weight:700;margin-top:4px}.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}.ct{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;margin-bottom:12px}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;padding:10px 12px;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid var(--border)}td{padding:12px;border-bottom:1px solid var(--border)}.btn{display:inline-flex;align-items:center;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--text);text-decoration:none}.btn-p{background:var(--primary);border-color:var(--primary);color:#fff}.btn-sm{padding:4px 10px;font-size:12px}.input{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px}.fg{margin-bottom:14px}.fl{display:block;font-size:12px;color:var(--dim);margin-bottom:4px}.empty{text-align:center;padding:48px 20px;color:var(--muted)}select.input{appearance:auto}";
|
|
72
|
+
|
|
73
|
+
string NavItem(string href, string icon, string label, string page) =>
|
|
74
|
+
$"<a href='{href}' class='{(page == label.ToLower() ? "on" : "")}'>{icon} <span>{label}</span></a>";
|
|
75
|
+
|
|
76
|
+
string Layout(string page, string userName, string userEmail, string content) => $@"<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1.0'><title>AgenticMail Enterprise ā .NET</title><style>{CSS}</style></head>
|
|
77
|
+
<body><div class='layout'>
|
|
78
|
+
<div class='sidebar'><div class='sh'><h2>š¢ <em>Agentic</em>Mail</h2><small>Enterprise Ā· .NET</small></div>
|
|
79
|
+
<div class='nav'><div class='ns'>Overview</div>{NavItem("/", "š", "Dashboard", page)}<div class='ns'>Manage</div>{NavItem("/agents", "š¤", "Agents", page)}{NavItem("/users", "š„", "Users", page)}{NavItem("/api-keys", "š", "API Keys", page)}<div class='ns'>System</div>{NavItem("/audit", "š", "Audit", page)}{NavItem("/settings", "āļø", "Settings", page)}</div>
|
|
80
|
+
<div class='sf'><div style='color:var(--dim)'>{Esc(userName)}</div><div style='color:var(--muted);font-size:11px'>{Esc(userEmail)}</div><a href='/logout' style='color:var(--muted);font-size:11px;margin-top:6px;display:inline-block'>Sign out</a></div></div>
|
|
81
|
+
<div class='content'>{content}</div></div></body></html>";
|
|
82
|
+
|
|
83
|
+
string Html(HttpContext ctx, string page, string content)
|
|
84
|
+
{
|
|
85
|
+
var name = ctx.Session.GetString("userName") ?? "";
|
|
86
|
+
var email = ctx.Session.GetString("userEmail") ?? "";
|
|
87
|
+
return Layout(page, name, email, content);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// āāā Auth āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
91
|
+
|
|
92
|
+
app.MapGet("/login", () => Results.Content($@"<!DOCTYPE html><html><head><meta charset='UTF-8'><title>AgenticMail</title><style>{CSS}</style></head>
|
|
93
|
+
<body style='display:flex;align-items:center;justify-content:center;min-height:100vh'>
|
|
94
|
+
<div style='width:380px'><h1 style='text-align:center;font-size:22px'>š¢ <em style='color:var(--primary)'>AgenticMail</em> Enterprise</h1>
|
|
95
|
+
<p style='text-align:center;color:var(--dim);font-size:13px;margin-bottom:32px'>Sign in Ā· .NET Dashboard</p>
|
|
96
|
+
<form method='POST' action='/login'><div class='fg'><label class='fl'>Email</label><input class='input' type='email' name='email' required></div>
|
|
97
|
+
<div class='fg'><label class='fl'>Password</label><input class='input' type='password' name='password' required></div>
|
|
98
|
+
<button class='btn btn-p' style='width:100%' type='submit'>Sign In</button></form></div></body></html>", "text/html"));
|
|
99
|
+
|
|
100
|
+
app.MapPost("/login", async (HttpContext ctx) =>
|
|
101
|
+
{
|
|
102
|
+
var form = await ctx.Request.ReadFormAsync();
|
|
103
|
+
var data = await Api(ctx, "/auth/login", "POST", new { email = form["email"].ToString(), password = form["password"].ToString() });
|
|
104
|
+
if (data?.TryGetProperty("token", out var tok) == true)
|
|
105
|
+
{
|
|
106
|
+
ctx.Session.SetString("token", tok.ToString());
|
|
107
|
+
if (data?.TryGetProperty("user", out var u) == true)
|
|
108
|
+
{
|
|
109
|
+
ctx.Session.SetString("userName", Str(u, "name"));
|
|
110
|
+
ctx.Session.SetString("userEmail", Str(u, "email"));
|
|
111
|
+
}
|
|
112
|
+
return Results.Redirect("/");
|
|
113
|
+
}
|
|
114
|
+
return Results.Content($"Login failed: {Str(data, "error")} <a href='/login'>Try again</a>", "text/html");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
app.MapGet("/logout", (HttpContext ctx) => { ctx.Session.Clear(); return Results.Redirect("/login"); });
|
|
118
|
+
|
|
119
|
+
// āāā Auth Guard āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
120
|
+
|
|
121
|
+
app.Use(async (ctx, next) =>
|
|
122
|
+
{
|
|
123
|
+
var path = ctx.Request.Path.Value ?? "";
|
|
124
|
+
if (path != "/login" && !path.StartsWith("/login") && ctx.Session.GetString("token") == null)
|
|
125
|
+
{
|
|
126
|
+
ctx.Response.Redirect("/login");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await next();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// āāā Dashboard āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
133
|
+
|
|
134
|
+
app.MapGet("/", async (HttpContext ctx) =>
|
|
135
|
+
{
|
|
136
|
+
var stats = await Api(ctx, "/api/stats");
|
|
137
|
+
var audit = await Api(ctx, "/api/audit?limit=8");
|
|
138
|
+
var events = "";
|
|
139
|
+
if (audit?.TryGetProperty("events", out var evArr) == true)
|
|
140
|
+
{
|
|
141
|
+
foreach (var e in evArr.EnumerateArray())
|
|
142
|
+
events += $"<div style='padding:10px 0;border-bottom:1px solid var(--border);font-size:13px'><span style='color:var(--primary);font-weight:500'>{Esc(Str(e, "action"))}</span> on {Esc(Str(e, "resource"))}<div style='font-size:11px;color:var(--muted)'>{Esc(Str(e, "timestamp"))}</div></div>";
|
|
143
|
+
}
|
|
144
|
+
if (string.IsNullOrEmpty(events)) events = "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>š</div>No activity yet</div>";
|
|
145
|
+
|
|
146
|
+
return Results.Content(Html(ctx, "dashboard",
|
|
147
|
+
$@"<h2 class='t'>Dashboard</h2><p class='desc'>Overview</p>
|
|
148
|
+
<div class='stats'>
|
|
149
|
+
<div class='stat'><div class='l'>Total Agents</div><div class='v' style='color:var(--primary)'>{Int(stats, "totalAgents")}</div></div>
|
|
150
|
+
<div class='stat'><div class='l'>Active Agents</div><div class='v' style='color:var(--success)'>{Int(stats, "activeAgents")}</div></div>
|
|
151
|
+
<div class='stat'><div class='l'>Users</div><div class='v'>{Int(stats, "totalUsers")}</div></div>
|
|
152
|
+
<div class='stat'><div class='l'>Audit Events</div><div class='v'>{Int(stats, "totalAuditEvents")}</div></div></div>
|
|
153
|
+
<div class='card'><div class='ct'>Recent Activity</div>{events}</div>"), "text/html");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// āāā Agents āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
157
|
+
|
|
158
|
+
app.MapGet("/agents", async (HttpContext ctx) =>
|
|
159
|
+
{
|
|
160
|
+
var data = await Api(ctx, "/api/agents");
|
|
161
|
+
var rows = "";
|
|
162
|
+
if (data?.TryGetProperty("agents", out var arr) == true)
|
|
163
|
+
{
|
|
164
|
+
foreach (var a in arr.EnumerateArray())
|
|
165
|
+
rows += $"<tr><td style='font-weight:600'>{Esc(Str(a, "name"))}</td><td style='color:var(--dim)'>{Esc(Str(a, "email"))}</td><td>{Esc(Str(a, "role"))}</td><td>{Badge(Str(a, "status"))}</td></tr>";
|
|
166
|
+
}
|
|
167
|
+
var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>š¤</div>No agents yet</div>" :
|
|
168
|
+
$"<table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>";
|
|
169
|
+
|
|
170
|
+
return Results.Content(Html(ctx, "agents",
|
|
171
|
+
$@"<h2 class='t'>Agents</h2><p class='desc'>Manage AI agent identities</p>
|
|
172
|
+
<div class='card' style='margin-bottom:16px'><div class='ct'>Create Agent</div>
|
|
173
|
+
<form method='POST' action='/agents' style='display:flex;gap:10px;align-items:end'>
|
|
174
|
+
<div class='fg' style='flex:1;margin:0'><label class='fl'>Name</label><input class='input' name='name' required placeholder='e.g. researcher'></div>
|
|
175
|
+
<div class='fg' style='margin:0'><label class='fl'>Role</label><select class='input' name='role'><option>assistant</option><option>researcher</option><option>writer</option></select></div>
|
|
176
|
+
<button class='btn btn-p' type='submit'>Create</button></form></div>
|
|
177
|
+
<div class='card'>{table}</div>"), "text/html");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
app.MapPost("/agents", async (HttpContext ctx) =>
|
|
181
|
+
{
|
|
182
|
+
var form = await ctx.Request.ReadFormAsync();
|
|
183
|
+
await Api(ctx, "/api/agents", "POST", new { name = form["name"].ToString(), role = form["role"].ToString() });
|
|
184
|
+
return Results.Redirect("/agents");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// āāā Users āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
188
|
+
|
|
189
|
+
app.MapGet("/users", async (HttpContext ctx) =>
|
|
190
|
+
{
|
|
191
|
+
var data = await Api(ctx, "/api/users");
|
|
192
|
+
var rows = "";
|
|
193
|
+
if (data?.TryGetProperty("users", out var arr) == true)
|
|
194
|
+
{
|
|
195
|
+
foreach (var u in arr.EnumerateArray())
|
|
196
|
+
rows += $"<tr><td style='font-weight:600'>{Esc(Str(u, "name"))}</td><td style='color:var(--dim)'>{Esc(Str(u, "email"))}</td><td>{Badge(Str(u, "role"))}</td></tr>";
|
|
197
|
+
}
|
|
198
|
+
var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>š„</div>No users yet</div>" :
|
|
199
|
+
$"<table><thead><tr><th>Name</th><th>Email</th><th>Role</th></tr></thead><tbody>{rows}</tbody></table>";
|
|
200
|
+
return Results.Content(Html(ctx, "users", $"<h2 class='t'>Users</h2><p class='desc'>Manage team members</p><div class='card'>{table}</div>"), "text/html");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// āāā API Keys āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
204
|
+
|
|
205
|
+
app.MapGet("/api-keys", async (HttpContext ctx) =>
|
|
206
|
+
{
|
|
207
|
+
var data = await Api(ctx, "/api/api-keys");
|
|
208
|
+
var rows = "";
|
|
209
|
+
if (data?.TryGetProperty("keys", out var arr) == true)
|
|
210
|
+
{
|
|
211
|
+
foreach (var k in arr.EnumerateArray())
|
|
212
|
+
{
|
|
213
|
+
var status = k.TryGetProperty("revoked", out var rev) && rev.GetBoolean() ? "revoked" : "active";
|
|
214
|
+
rows += $"<tr><td style='font-weight:600'>{Esc(Str(k, "name"))}</td><td><code style='font-size:12px'>{Esc(Str(k, "keyPrefix"))}...</code></td><td>{Badge(status)}</td></tr>";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>š</div>No API keys</div>" :
|
|
218
|
+
$"<table><thead><tr><th>Name</th><th>Key</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>";
|
|
219
|
+
return Results.Content(Html(ctx, "api keys", $"<h2 class='t'>API Keys</h2><p class='desc'>Manage programmatic access</p><div class='card'>{table}</div>"), "text/html");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// āāā Audit āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
223
|
+
|
|
224
|
+
app.MapGet("/audit", async (HttpContext ctx) =>
|
|
225
|
+
{
|
|
226
|
+
var data = await Api(ctx, "/api/audit?limit=25");
|
|
227
|
+
var total = Int(data, "total");
|
|
228
|
+
var rows = "";
|
|
229
|
+
if (data?.TryGetProperty("events", out var arr) == true)
|
|
230
|
+
{
|
|
231
|
+
foreach (var e in arr.EnumerateArray())
|
|
232
|
+
rows += $"<tr><td style='font-size:12px;color:var(--muted)'>{Esc(Str(e, "timestamp"))}</td><td>{Esc(Str(e, "actor"))}</td><td style='color:var(--primary);font-weight:500'>{Esc(Str(e, "action"))}</td><td style='font-size:12px'>{Esc(Str(e, "resource"))}</td></tr>";
|
|
233
|
+
}
|
|
234
|
+
var table = string.IsNullOrEmpty(rows) ? "<div class='empty'><div style='font-size:36px;margin-bottom:10px'>š</div>No audit events</div>" :
|
|
235
|
+
$"<table><thead><tr><th>Time</th><th>Actor</th><th>Action</th><th>Resource</th></tr></thead><tbody>{rows}</tbody></table>";
|
|
236
|
+
return Results.Content(Html(ctx, "audit", $"<h2 class='t'>Audit Log</h2><p class='desc'>{total} events</p><div class='card'>{table}</div>"), "text/html");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// āāā Settings āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
240
|
+
|
|
241
|
+
app.MapGet("/settings", async (HttpContext ctx) =>
|
|
242
|
+
{
|
|
243
|
+
var s = await Api(ctx, "/api/settings");
|
|
244
|
+
return Results.Content(Html(ctx, "settings",
|
|
245
|
+
$@"<h2 class='t'>Settings</h2><p class='desc'>Configure your organization</p>
|
|
246
|
+
<div class='card'><div class='ct'>General</div><div style='font-size:13px'>
|
|
247
|
+
Name: {Esc(Str(s, "name"))}<br>Domain: {Esc(Str(s, "domain"))}<br>
|
|
248
|
+
Plan: {Badge(Str(s, "plan").ToUpper())}<br>
|
|
249
|
+
Subdomain: {Esc(Str(s, "subdomain"))}.agenticmail.cloud</div></div>"), "text/html");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// āāā Start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
253
|
+
|
|
254
|
+
var port = Environment.GetEnvironmentVariable("PORT") ?? "5002";
|
|
255
|
+
app.Urls.Add($"http://localhost:{port}");
|
|
256
|
+
|
|
257
|
+
Console.WriteLine($"\nš¢ AgenticMail Enterprise Dashboard (.NET)");
|
|
258
|
+
Console.WriteLine($" API: {API_URL}");
|
|
259
|
+
Console.WriteLine($" Dashboard: http://localhost:{port}\n");
|
|
260
|
+
|
|
261
|
+
app.Run();
|