@omg-dev/admin 0.4.24
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/README.md +126 -0
- package/dist/index.mjs +2 -0
- package/dist/react-COCR3YSL.mjs +1603 -0
- package/dist/server.mjs +50 -0
- package/dist/standalone.mjs +11 -0
- package/package.json +47 -0
- package/src/client.ts +278 -0
- package/src/index.ts +43 -0
- package/src/react.tsx +983 -0
- package/src/server.ts +83 -0
- package/src/standalone.tsx +25 -0
- package/src/styles.ts +200 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @omg-dev/admin/server — mountable backend proxy for the admin console.
|
|
2
|
+
//
|
|
3
|
+
// The admin console must NOT hold the omg control-plane token in the browser.
|
|
4
|
+
// Instead the HOST APP's backend mounts this proxy: the browser calls a
|
|
5
|
+
// same-origin path (default "/_omg/..."), and the proxy injects the admin
|
|
6
|
+
// bearer token SERVER-SIDE and forwards to the control-plane. The token lives
|
|
7
|
+
// in the host's server env, never in the client bundle.
|
|
8
|
+
//
|
|
9
|
+
// Web-standard handler (Request → Response) so it drops into Bun.serve, the
|
|
10
|
+
// Fetch API, Hono, Next route handlers, etc. Returns `null` when the path is
|
|
11
|
+
// outside the mount prefix, so the host can fall through to its own routes:
|
|
12
|
+
//
|
|
13
|
+
// import { createOmgAdminProxy } from "@omg-dev/admin/server"
|
|
14
|
+
// const omgAdmin = createOmgAdminProxy({ token: process.env.OMG_ADMIN_TOKEN! })
|
|
15
|
+
// // inside fetch(req):
|
|
16
|
+
// const res = await omgAdmin(req); if (res) return res;
|
|
17
|
+
//
|
|
18
|
+
// Then point the console at the mount: <AdminConsole config={{ apiBase: "/_omg",
|
|
19
|
+
// getToken: () => null }} /> — auth is added by the proxy, not the client.
|
|
20
|
+
|
|
21
|
+
export interface OmgAdminProxyOptions {
|
|
22
|
+
/** omg admin bearer JWT (an auth.omg.dev token for an allowlisted admin). */
|
|
23
|
+
token: string;
|
|
24
|
+
/** Control-plane origin. Default https://backend.omg.dev. */
|
|
25
|
+
target?: string;
|
|
26
|
+
/** Same-origin mount prefix the console's apiBase points at. Default "/_omg". */
|
|
27
|
+
prefix?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The proxy only forwards the admin surface — never arbitrary control-plane
|
|
31
|
+
// paths — so a leaked mount can't wield the powerful admin token against other
|
|
32
|
+
// endpoints. Keep in sync with the console's AdminClient modules.
|
|
33
|
+
const ALLOWED = ["/api/flags/", "/api/adminBilling/", "/api/bugReports/", "/api/users/"];
|
|
34
|
+
|
|
35
|
+
function json(body: unknown, status: number): Response {
|
|
36
|
+
return new Response(JSON.stringify(body), {
|
|
37
|
+
status,
|
|
38
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createOmgAdminProxy(opts: OmgAdminProxyOptions) {
|
|
43
|
+
const target = (opts.target ?? "https://backend.omg.dev").replace(/\/$/, "");
|
|
44
|
+
const prefix = opts.prefix ?? "/_omg";
|
|
45
|
+
|
|
46
|
+
return async function handle(req: Request): Promise<Response | null> {
|
|
47
|
+
const url = new URL(req.url);
|
|
48
|
+
if (url.pathname !== prefix && !url.pathname.startsWith(prefix + "/")) {
|
|
49
|
+
return null; // not ours — host falls through
|
|
50
|
+
}
|
|
51
|
+
if (!opts.token) return json({ error: "omg admin token not configured" }, 503);
|
|
52
|
+
|
|
53
|
+
const upstreamPath = url.pathname.slice(prefix.length); // e.g. "/api/flags/listFlags"
|
|
54
|
+
if (!ALLOWED.some((p) => upstreamPath.startsWith(p))) {
|
|
55
|
+
return json({ error: "forbidden path" }, 403);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const init: RequestInit = {
|
|
59
|
+
method: req.method,
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": req.headers.get("content-type") ?? "application/json",
|
|
62
|
+
authorization: `Bearer ${opts.token}`,
|
|
63
|
+
},
|
|
64
|
+
signal: AbortSignal.timeout(30_000),
|
|
65
|
+
};
|
|
66
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
67
|
+
init.body = await req.text();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const r = await fetch(`${target}${upstreamPath}${url.search}`, init);
|
|
72
|
+
return new Response(r.body, {
|
|
73
|
+
status: r.status,
|
|
74
|
+
headers: {
|
|
75
|
+
"content-type": r.headers.get("content-type") ?? "application/json",
|
|
76
|
+
"cache-control": "no-store",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
return json({ error: "omg control-plane unreachable" }, 502);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @omg-dev/admin/standalone — run the console as its own app.
|
|
2
|
+
//
|
|
3
|
+
// The SAME <AdminConsole/> embedded as a tab elsewhere, but with a tiny shell
|
|
4
|
+
// that mounts it into a DOM node. The host still supplies auth (apiBase +
|
|
5
|
+
// getToken) — standalone just means "this is the whole page", not "no auth".
|
|
6
|
+
//
|
|
7
|
+
// import { mountAdminConsole } from "@omg-dev/admin/standalone"
|
|
8
|
+
// mountAdminConsole(document.getElementById("root")!, {
|
|
9
|
+
// apiBase: "https://backend.omg.dev",
|
|
10
|
+
// getToken: async () => myAuth.getToken(),
|
|
11
|
+
// })
|
|
12
|
+
|
|
13
|
+
import { createRoot, type Root } from "react-dom/client"
|
|
14
|
+
import { createElement } from "react"
|
|
15
|
+
import { AdminConsole } from "./react"
|
|
16
|
+
import { AdminClient, type AdminClientConfig } from "./client"
|
|
17
|
+
|
|
18
|
+
export function mountAdminConsole(el: HTMLElement, config: AdminClientConfig): Root {
|
|
19
|
+
const root = createRoot(el)
|
|
20
|
+
root.render(createElement(AdminConsole, { client: new AdminClient(config) }))
|
|
21
|
+
return root
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { AdminConsole, AdminClient }
|
|
25
|
+
export type { AdminClientConfig }
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @omg-dev/admin — self-contained stylesheet, injected once at first mount.
|
|
2
|
+
//
|
|
3
|
+
// Same reasoning as @omg-dev/pwa: Tailwind v4 does not scan
|
|
4
|
+
// node_modules, so utility classes shipped from an npm package only render by
|
|
5
|
+
// luck. This panel ships one `<style>` tag of plain, namespaced (`.vadm-*`)
|
|
6
|
+
// CSS. Colors read the host's shadcn CSS variables first (--background,
|
|
7
|
+
// --foreground, --border, --primary, --muted-foreground) and fall back to
|
|
8
|
+
// neutral defaults with a prefers-color-scheme dark variant — so the console
|
|
9
|
+
// looks native embedded in a themed app and presentable in a bare one.
|
|
10
|
+
|
|
11
|
+
export const STYLE_ID = "vibes-admin-styles"
|
|
12
|
+
|
|
13
|
+
export function ensureStyles() {
|
|
14
|
+
if (typeof document === "undefined") return
|
|
15
|
+
if (document.getElementById(STYLE_ID)) return
|
|
16
|
+
const el = document.createElement("style")
|
|
17
|
+
el.id = STYLE_ID
|
|
18
|
+
el.textContent = CSS
|
|
19
|
+
document.head.appendChild(el)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CSS = `
|
|
23
|
+
.vadm {
|
|
24
|
+
--vadm-bg: var(--background, #ffffff);
|
|
25
|
+
--vadm-fg: var(--foreground, #18181b);
|
|
26
|
+
--vadm-muted: var(--muted-foreground, #71717a);
|
|
27
|
+
--vadm-border: var(--border, #e4e4e7);
|
|
28
|
+
--vadm-accent: var(--primary, #2563eb);
|
|
29
|
+
--vadm-accent-fg: var(--primary-foreground, #ffffff);
|
|
30
|
+
--vadm-card: var(--card, #fafafa);
|
|
31
|
+
--vadm-danger: #dc2626;
|
|
32
|
+
color: var(--vadm-fg);
|
|
33
|
+
font-family: inherit;
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
line-height: 1.5;
|
|
36
|
+
}
|
|
37
|
+
.vadm, .vadm * { box-sizing: border-box; }
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
.vadm {
|
|
40
|
+
--vadm-bg: var(--background, #0a0a0a);
|
|
41
|
+
--vadm-fg: var(--foreground, #f4f4f5);
|
|
42
|
+
--vadm-muted: var(--muted-foreground, #a1a1aa);
|
|
43
|
+
--vadm-border: var(--border, #27272a);
|
|
44
|
+
--vadm-card: var(--card, #18181b);
|
|
45
|
+
--vadm-danger: #f87171;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.vadm-tabs {
|
|
50
|
+
display: flex; gap: 3px; border: 1px solid var(--vadm-border);
|
|
51
|
+
background: color-mix(in srgb, var(--vadm-muted) 9%, transparent);
|
|
52
|
+
border-radius: 8px; padding: 3px; margin-bottom: 16px; overflow-x: auto;
|
|
53
|
+
scrollbar-width: none;
|
|
54
|
+
}
|
|
55
|
+
.vadm-tabs::-webkit-scrollbar { display: none; }
|
|
56
|
+
.vadm-tab {
|
|
57
|
+
appearance: none; background: none; border: none; cursor: pointer;
|
|
58
|
+
padding: 7px 12px; color: var(--vadm-muted); font: inherit; font-weight: 600;
|
|
59
|
+
border-radius: 6px; white-space: nowrap; flex: none;
|
|
60
|
+
}
|
|
61
|
+
.vadm-tab[data-active="true"] {
|
|
62
|
+
color: var(--vadm-fg); background: var(--vadm-bg);
|
|
63
|
+
box-shadow: 0 1px 2px color-mix(in srgb, var(--vadm-fg) 10%, transparent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.vadm-row { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
67
|
+
.vadm-grow { flex: 1; min-width: 0; }
|
|
68
|
+
.vadm-spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
|
|
69
|
+
.vadm-muted { color: var(--vadm-muted); }
|
|
70
|
+
.vadm-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
71
|
+
.vadm-h { font-weight: 600; font-size: 15px; margin: 0 0 4px; }
|
|
72
|
+
.vadm-sub { color: var(--vadm-muted); font-size: 13px; margin: 0 0 16px; }
|
|
73
|
+
|
|
74
|
+
.vadm-card {
|
|
75
|
+
border: 1px solid var(--vadm-border); border-radius: 8px;
|
|
76
|
+
background: var(--vadm-card); padding: 14px 16px; margin-bottom: 10px;
|
|
77
|
+
}
|
|
78
|
+
.vadm-card[data-off="true"] { opacity: 0.6; }
|
|
79
|
+
|
|
80
|
+
.vadm-btn {
|
|
81
|
+
appearance: none; cursor: pointer; font: inherit; font-weight: 500;
|
|
82
|
+
border: 1px solid var(--vadm-border); border-radius: 8px;
|
|
83
|
+
background: var(--vadm-bg); color: var(--vadm-fg); padding: 6px 12px;
|
|
84
|
+
min-height: 34px; white-space: nowrap;
|
|
85
|
+
}
|
|
86
|
+
.vadm-btn:hover { border-color: var(--vadm-accent); }
|
|
87
|
+
.vadm-btn[data-primary="true"] { background: var(--vadm-accent); color: var(--vadm-accent-fg); border-color: var(--vadm-accent); }
|
|
88
|
+
.vadm-btn[data-danger="true"] { color: var(--vadm-danger); border-color: var(--vadm-danger); }
|
|
89
|
+
.vadm-btn:disabled { opacity: 0.5; cursor: default; }
|
|
90
|
+
.vadm-btn-sm { padding: 3px 8px; font-size: 12px; border-radius: 6px; }
|
|
91
|
+
|
|
92
|
+
.vadm-input, .vadm-select, .vadm-textarea {
|
|
93
|
+
font: inherit; color: var(--vadm-fg); background: var(--vadm-bg);
|
|
94
|
+
border: 1px solid var(--vadm-border); border-radius: 8px; padding: 7px 10px; width: 100%;
|
|
95
|
+
min-height: 36px;
|
|
96
|
+
}
|
|
97
|
+
.vadm-input:focus, .vadm-select:focus, .vadm-textarea:focus { outline: none; border-color: var(--vadm-accent); }
|
|
98
|
+
.vadm-textarea { resize: vertical; min-height: 56px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
99
|
+
|
|
100
|
+
.vadm-field { margin-bottom: 10px; }
|
|
101
|
+
.vadm-label { display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px; color: var(--vadm-muted); }
|
|
102
|
+
|
|
103
|
+
/* iOS-style switch */
|
|
104
|
+
.vadm-switch { position: relative; display: inline-block; width: 38px; height: 22px; flex: none; }
|
|
105
|
+
.vadm-switch input { opacity: 0; width: 0; height: 0; }
|
|
106
|
+
.vadm-slider {
|
|
107
|
+
position: absolute; inset: 0; cursor: pointer; border-radius: 999px;
|
|
108
|
+
background: var(--vadm-border); transition: background 0.15s;
|
|
109
|
+
}
|
|
110
|
+
.vadm-slider::before {
|
|
111
|
+
content: ""; position: absolute; height: 16px; width: 16px; left: 3px; top: 3px;
|
|
112
|
+
background: #fff; border-radius: 50%; transition: transform 0.15s;
|
|
113
|
+
}
|
|
114
|
+
.vadm-switch input:checked + .vadm-slider { background: var(--vadm-accent); }
|
|
115
|
+
.vadm-switch input:checked + .vadm-slider::before { transform: translateX(16px); }
|
|
116
|
+
|
|
117
|
+
.vadm-badge {
|
|
118
|
+
display: inline-block; font-size: 11px; font-weight: 500; padding: 1px 7px;
|
|
119
|
+
border-radius: 999px; border: 1px solid var(--vadm-border); color: var(--vadm-muted);
|
|
120
|
+
}
|
|
121
|
+
.vadm-badge[data-on="true"] { color: var(--vadm-accent); border-color: var(--vadm-accent); }
|
|
122
|
+
|
|
123
|
+
.vadm-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
124
|
+
.vadm-table th { text-align: left; color: var(--vadm-muted); font-weight: 500; padding: 6px 8px; border-bottom: 1px solid var(--vadm-border); }
|
|
125
|
+
.vadm-table td { padding: 6px 8px; border-bottom: 1px solid var(--vadm-border); }
|
|
126
|
+
|
|
127
|
+
.vadm-empty { text-align: center; color: var(--vadm-muted); padding: 32px 0; }
|
|
128
|
+
.vadm-err { color: var(--vadm-danger); background: color-mix(in srgb, var(--vadm-danger) 10%, transparent); border: 1px solid var(--vadm-danger); border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; }
|
|
129
|
+
.vadm-detail { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--vadm-border); }
|
|
130
|
+
|
|
131
|
+
.vadm-panel-head { align-items: flex-start; margin-bottom: 12px; }
|
|
132
|
+
.vadm-search { display: flex; align-items: center; gap: 8px; width: min(100%, 420px); }
|
|
133
|
+
.vadm-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); gap: 14px; align-items: start; }
|
|
134
|
+
.vadm-list { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
135
|
+
.vadm-user-row {
|
|
136
|
+
appearance: none; width: 100%; border: 1px solid var(--vadm-border); border-radius: 8px;
|
|
137
|
+
background: var(--vadm-bg); color: var(--vadm-fg); padding: 10px;
|
|
138
|
+
display: flex; align-items: center; gap: 10px; text-align: left; font: inherit;
|
|
139
|
+
}
|
|
140
|
+
button.vadm-user-row { cursor: pointer; }
|
|
141
|
+
.vadm-user-row[data-active="true"] {
|
|
142
|
+
border-color: color-mix(in srgb, var(--vadm-accent) 55%, var(--vadm-border));
|
|
143
|
+
background: color-mix(in srgb, var(--vadm-accent) 8%, var(--vadm-bg));
|
|
144
|
+
}
|
|
145
|
+
.vadm-avatar {
|
|
146
|
+
width: 38px; height: 38px; border-radius: 50%; flex: none;
|
|
147
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
148
|
+
background: color-mix(in srgb, var(--vadm-accent) 13%, var(--vadm-bg));
|
|
149
|
+
color: var(--vadm-accent); font-weight: 700; font-size: 12px; overflow: hidden;
|
|
150
|
+
}
|
|
151
|
+
.vadm-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
152
|
+
.vadm-user-name { display: block; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
153
|
+
.vadm-user-stats { color: var(--vadm-muted); font-size: 12px; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
|
|
154
|
+
.vadm-inspector { position: sticky; top: 12px; margin-bottom: 0; }
|
|
155
|
+
.vadm-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin: 14px 0; }
|
|
156
|
+
.vadm-metrics div {
|
|
157
|
+
border: 1px solid var(--vadm-border); border-radius: 8px; padding: 8px; background: var(--vadm-bg);
|
|
158
|
+
}
|
|
159
|
+
.vadm-metrics strong { display: block; font-size: 18px; line-height: 1.1; }
|
|
160
|
+
.vadm-metrics span { display: block; color: var(--vadm-muted); font-size: 11px; margin-top: 2px; }
|
|
161
|
+
.vadm-kv { display: grid; gap: 8px; }
|
|
162
|
+
.vadm-kv > div { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: 10px; align-items: baseline; }
|
|
163
|
+
.vadm-kv span { color: var(--vadm-muted); font-size: 12px; }
|
|
164
|
+
.vadm-kv strong { font-size: 13px; font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
|
|
165
|
+
.vadm-kv code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow-wrap: anywhere; }
|
|
166
|
+
.vadm-skel {
|
|
167
|
+
display: block; height: 12px; border-radius: 999px;
|
|
168
|
+
background: color-mix(in srgb, var(--vadm-muted) 18%, transparent);
|
|
169
|
+
animation: vadmPulse 1.4s ease-in-out infinite;
|
|
170
|
+
}
|
|
171
|
+
.vadm-skel-avatar { width: 38px; height: 38px; border-radius: 50%; flex: none; }
|
|
172
|
+
@keyframes vadmPulse {
|
|
173
|
+
0%, 100% { opacity: 0.55; }
|
|
174
|
+
50% { opacity: 1; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@media (max-width: 760px) {
|
|
178
|
+
.vadm { font-size: 13px; }
|
|
179
|
+
.vadm-tabs { margin-left: -2px; margin-right: -2px; }
|
|
180
|
+
.vadm-row, .vadm-spread { flex-wrap: wrap; }
|
|
181
|
+
.vadm-panel-head { display: block; }
|
|
182
|
+
.vadm-search { width: 100%; margin-top: 10px; }
|
|
183
|
+
.vadm-search .vadm-input { min-width: 0; }
|
|
184
|
+
.vadm-split { display: flex; flex-direction: column; }
|
|
185
|
+
.vadm-inspector { position: static; width: 100% !important; max-width: 100%; }
|
|
186
|
+
.vadm-user-row { align-items: flex-start; flex-wrap: wrap; }
|
|
187
|
+
.vadm-user-stats { flex-direction: row; align-items: center; gap: 8px; width: 100%; padding-left: 48px; margin-top: -4px; }
|
|
188
|
+
.vadm-table { display: block; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
|
|
189
|
+
.vadm-detail.vadm-card { width: 100% !important; max-width: 100%; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@media (max-width: 480px) {
|
|
193
|
+
.vadm-card { padding: 12px; }
|
|
194
|
+
.vadm-search { flex-wrap: wrap; }
|
|
195
|
+
.vadm-search .vadm-input { flex: 1 0 100%; }
|
|
196
|
+
.vadm-btn { min-height: 36px; }
|
|
197
|
+
.vadm-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
198
|
+
.vadm-kv > div { grid-template-columns: 1fr; gap: 1px; }
|
|
199
|
+
}
|
|
200
|
+
`
|