feat(phase1d): sase admin-sdk skeleton + endpoint inventory on /projects/sase
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { saseDb } from "@/lib/db-sase";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SASE_ADMIN_ENDPOINTS, saseAdminWired } from "@/lib/admin-sdk/sase";
|
||||
|
||||
async function getSaseStats() {
|
||||
try {
|
||||
@@ -37,21 +38,52 @@ export async function SaseHealth() {
|
||||
);
|
||||
}
|
||||
|
||||
const wired = saseAdminWired();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Stat label="Connection" value="healthy" badge="ok" />
|
||||
<Stat label="Total users" value={stats.users.toString()} hint="public.users" />
|
||||
<Stat
|
||||
label="New users (7d)"
|
||||
value={stats.recentUsers.toString()}
|
||||
hint="created_at last 7 days"
|
||||
/>
|
||||
<Stat
|
||||
label="Active subscriptions"
|
||||
value={`${stats.activeSubs} / ${stats.totalSubs}`}
|
||||
hint="status='active'"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Stat label="Connection" value="healthy" badge="ok" />
|
||||
<Stat label="Total users" value={stats.users.toString()} hint="public.users" />
|
||||
<Stat
|
||||
label="New users (7d)"
|
||||
value={stats.recentUsers.toString()}
|
||||
hint="created_at last 7 days"
|
||||
/>
|
||||
<Stat
|
||||
label="Active subscriptions"
|
||||
value={`${stats.activeSubs} / ${stats.totalSubs}`}
|
||||
hint="status='active'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>Admin SDK</CardDescription>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
{wired ? (
|
||||
<Badge>{SASE_ADMIN_ENDPOINTS.length} endpoints wired</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">not wired</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{!wired && (
|
||||
<p className="text-muted-foreground">
|
||||
Set <code>SASE_ADMIN_API_BASE</code> and{" "}
|
||||
<code>INTERNAL_API_TOKEN_SASE</code> in Coolify env when spoke ships
|
||||
the <code>/internal/admin/*</code> endpoints.
|
||||
</p>
|
||||
)}
|
||||
<ul className="space-y-1 font-mono text-xs text-muted-foreground">
|
||||
{SASE_ADMIN_ENDPOINTS.map((e) => (
|
||||
<li key={e}>{e}</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
66
apps/web/src/lib/admin-sdk/sase.ts
Normal file
66
apps/web/src/lib/admin-sdk/sase.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { AdminClient } from "./base";
|
||||
|
||||
export type SaseUserRef = { id: string; email: string };
|
||||
export type SaseSubscriptionStatus = "pending" | "active" | "cancelled" | "expired";
|
||||
|
||||
export type SaseAdmin = {
|
||||
/**
|
||||
* Update a user's subscription. Spoke endpoint: PATCH /internal/admin/users/:id/subscription
|
||||
* Not yet implemented spoke-side.
|
||||
*/
|
||||
setSubscription(input: {
|
||||
userId: string;
|
||||
planId: string;
|
||||
billingPeriod: "monthly" | "yearly";
|
||||
}): Promise<{ id: string; status: SaseSubscriptionStatus }>;
|
||||
|
||||
/**
|
||||
* Force-cancel a subscription. Spoke endpoint: DELETE /internal/admin/subscriptions/:id
|
||||
*/
|
||||
cancelSubscription(subscriptionId: string): Promise<{ ok: true }>;
|
||||
|
||||
/**
|
||||
* Resend verification email. Spoke endpoint: POST /internal/admin/users/:id/resend-verification
|
||||
*/
|
||||
resendVerification(userId: string): Promise<{ ok: true }>;
|
||||
};
|
||||
|
||||
export function createSaseAdmin(): SaseAdmin {
|
||||
const base = process.env.SASE_ADMIN_API_BASE;
|
||||
const token = process.env.INTERNAL_API_TOKEN_SASE;
|
||||
if (!base || !token) {
|
||||
return notWiredSdk("sase");
|
||||
}
|
||||
const client = new AdminClient({ projectKey: "sase", baseUrl: base, token });
|
||||
return {
|
||||
setSubscription: (input) =>
|
||||
client.call("PATCH", `/internal/admin/users/${input.userId}/subscription`, input),
|
||||
cancelSubscription: (id) =>
|
||||
client.call("DELETE", `/internal/admin/subscriptions/${id}`),
|
||||
resendVerification: (id) =>
|
||||
client.call("POST", `/internal/admin/users/${id}/resend-verification`),
|
||||
};
|
||||
}
|
||||
|
||||
function notWiredSdk(projectKey: string): SaseAdmin {
|
||||
const reject = () => {
|
||||
throw new Error(
|
||||
`admin-sdk[${projectKey}]: spoke not wired (SASE_ADMIN_API_BASE / INTERNAL_API_TOKEN_SASE missing)`,
|
||||
);
|
||||
};
|
||||
return {
|
||||
setSubscription: () => Promise.reject(reject()),
|
||||
cancelSubscription: () => Promise.reject(reject()),
|
||||
resendVerification: () => Promise.reject(reject()),
|
||||
};
|
||||
}
|
||||
|
||||
export function saseAdminWired(): boolean {
|
||||
return Boolean(process.env.SASE_ADMIN_API_BASE && process.env.INTERNAL_API_TOKEN_SASE);
|
||||
}
|
||||
|
||||
export const SASE_ADMIN_ENDPOINTS = [
|
||||
"PATCH /internal/admin/users/:id/subscription",
|
||||
"DELETE /internal/admin/subscriptions/:id",
|
||||
"POST /internal/admin/users/:id/resend-verification",
|
||||
];
|
||||
Reference in New Issue
Block a user