@gigamusic/admin 0.1.0
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 +18 -0
- package/package.json +69 -0
- package/src/client.ts +18 -0
- package/src/components/AdminLoginForm.tsx +63 -0
- package/src/components/AdminNav.tsx +50 -0
- package/src/components/ReleaseForm.tsx +556 -0
- package/src/handlers/auth.ts +58 -0
- package/src/handlers/links.ts +117 -0
- package/src/handlers/orders.ts +21 -0
- package/src/handlers/releases.ts +209 -0
- package/src/handlers/settings.ts +54 -0
- package/src/handlers/upload.ts +286 -0
- package/src/index.ts +24 -0
- package/src/lib/rate-limit.ts +31 -0
- package/src/lib/responses.ts +19 -0
- package/src/lib/session.ts +89 -0
- package/src/lib/types.ts +45 -0
- package/src/pages/AdminEditReleasePage.tsx +39 -0
- package/src/pages/AdminHomePage.tsx +38 -0
- package/src/pages/AdminLayout.tsx +20 -0
- package/src/pages/AdminLinksPage.tsx +187 -0
- package/src/pages/AdminLoginPage.tsx +11 -0
- package/src/pages/AdminNewReleasePage.tsx +12 -0
- package/src/pages/AdminOrdersPage.tsx +127 -0
- package/src/pages/AdminReleasesPage.tsx +87 -0
- package/src/pages/AdminSettingsPage.tsx +84 -0
- package/src/server.ts +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @gigamusic/admin
|
|
2
|
+
|
|
3
|
+
Source-shipped React admin for the gigamusic platform: releases CRUD,
|
|
4
|
+
settings, links, link-pages, and orders view — plus per-route handler
|
|
5
|
+
factories the consumer wires into `/api/admin/*`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// app/admin/page.tsx (consumer)
|
|
9
|
+
export { AdminHomePage as default } from "@gigamusic/admin";
|
|
10
|
+
|
|
11
|
+
// app/api/admin/auth/route.ts (consumer)
|
|
12
|
+
import { createAdminLoginHandler } from "@gigamusic/admin/server";
|
|
13
|
+
export const POST = createAdminLoginHandler({ adminPasswordHash, adminSessionSecret });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Every page is auth-gated by the shared admin session cookie. Override a slot
|
|
17
|
+
via the standard `<GigamusicProvider components={{ Button: MyButton }}>` from
|
|
18
|
+
`@gigamusic/ui`, or replace a page wholesale by simply not mounting it.
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gigamusic/admin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Admin UI (releases CRUD, tracks, settings, links, link-pages, orders) and admin API handler factories for the gigamusic platform. Fully replaceable by the consumer — mount the pages directly or override individual slots.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/gigamesh/gigamusic.git",
|
|
9
|
+
"directory": "packages/admin"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.ts",
|
|
17
|
+
"default": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./client": {
|
|
20
|
+
"types": "./src/client.ts",
|
|
21
|
+
"default": "./src/client.ts"
|
|
22
|
+
},
|
|
23
|
+
"./server": {
|
|
24
|
+
"types": "./src/server.ts",
|
|
25
|
+
"default": "./src/server.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"zod": "^3.24.1",
|
|
37
|
+
"@gigamusic/email": "0.1.0",
|
|
38
|
+
"@gigamusic/config": "0.1.0",
|
|
39
|
+
"@gigamusic/storage": "0.1.0",
|
|
40
|
+
"@gigamusic/core": "0.1.0",
|
|
41
|
+
"@gigamusic/audio": "0.1.0",
|
|
42
|
+
"@gigamusic/db": "0.1.0",
|
|
43
|
+
"@gigamusic/ui": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"next": ">=15",
|
|
47
|
+
"react": ">=18 <20",
|
|
48
|
+
"react-dom": ">=18 <20"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@testing-library/dom": "^10.4.0",
|
|
52
|
+
"@testing-library/react": "^16.1.0",
|
|
53
|
+
"@types/node": "^22.10.5",
|
|
54
|
+
"@types/react": "^19.0.0",
|
|
55
|
+
"@types/react-dom": "^19.0.0",
|
|
56
|
+
"jsdom": "^25.0.1",
|
|
57
|
+
"next": "^16.0.0",
|
|
58
|
+
"react": "^19.0.0",
|
|
59
|
+
"react-dom": "^19.0.0",
|
|
60
|
+
"typescript": "^5.7.3",
|
|
61
|
+
"vitest": "^2.1.8"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"lint": "eslint src",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"clean": "rm -rf .turbo *.tsbuildinfo"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Client-only re-exports — every consumer-side page component lives here so
|
|
4
|
+
// importing them from a server-component context never accidentally pulls in
|
|
5
|
+
// server-only modules.
|
|
6
|
+
|
|
7
|
+
export { AdminHomePage } from "./pages/AdminHomePage";
|
|
8
|
+
export { AdminLoginPage } from "./pages/AdminLoginPage";
|
|
9
|
+
export { AdminReleasesPage } from "./pages/AdminReleasesPage";
|
|
10
|
+
export { AdminNewReleasePage } from "./pages/AdminNewReleasePage";
|
|
11
|
+
export { AdminEditReleasePage } from "./pages/AdminEditReleasePage";
|
|
12
|
+
export { AdminLinksPage } from "./pages/AdminLinksPage";
|
|
13
|
+
export { AdminSettingsPage } from "./pages/AdminSettingsPage";
|
|
14
|
+
export { AdminOrdersPage } from "./pages/AdminOrdersPage";
|
|
15
|
+
export { ReleaseForm } from "./components/ReleaseForm";
|
|
16
|
+
export type { ReleaseFormProps } from "./components/ReleaseForm";
|
|
17
|
+
export { AdminLoginForm } from "./components/AdminLoginForm";
|
|
18
|
+
export { AdminNav } from "./components/AdminNav";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import { useSlot } from "@gigamusic/ui/client";
|
|
6
|
+
|
|
7
|
+
/** Block off-site redirects via the `?next=` param. */
|
|
8
|
+
function safeNext(value: string | null): string | null {
|
|
9
|
+
if (!value) return null;
|
|
10
|
+
if (!value.startsWith("/")) return null;
|
|
11
|
+
if (value.startsWith("//") || value.startsWith("/\\")) return null;
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AdminLoginForm() {
|
|
16
|
+
const Button = useSlot("Button");
|
|
17
|
+
const [password, setPassword] = useState("");
|
|
18
|
+
const [error, setError] = useState("");
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const searchParams = useSearchParams();
|
|
22
|
+
const next = safeNext(searchParams.get("next"));
|
|
23
|
+
|
|
24
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError("");
|
|
28
|
+
const res = await fetch("/api/admin/auth", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ password }),
|
|
32
|
+
});
|
|
33
|
+
if (res.ok) {
|
|
34
|
+
if (next) router.push(next);
|
|
35
|
+
else router.refresh();
|
|
36
|
+
} else {
|
|
37
|
+
setError("Invalid password");
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<form onSubmit={handleSubmit} className="gm-admin-login space-y-4">
|
|
44
|
+
<div className="space-y-2">
|
|
45
|
+
<label htmlFor="password" className="text-sm font-medium">
|
|
46
|
+
Password
|
|
47
|
+
</label>
|
|
48
|
+
<input
|
|
49
|
+
id="password"
|
|
50
|
+
type="password"
|
|
51
|
+
value={password}
|
|
52
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
53
|
+
required
|
|
54
|
+
className="block w-full rounded border border-border bg-background px-3 py-2 text-sm"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
58
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
59
|
+
{loading ? "Logging in..." : "Log In"}
|
|
60
|
+
</Button>
|
|
61
|
+
</form>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
interface NavLink {
|
|
7
|
+
href: string;
|
|
8
|
+
label: string;
|
|
9
|
+
exact?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_LINKS: NavLink[] = [
|
|
13
|
+
{ href: "/admin", label: "Releases", exact: true },
|
|
14
|
+
{ href: "/admin/orders", label: "Orders" },
|
|
15
|
+
{ href: "/admin/links", label: "Links" },
|
|
16
|
+
{ href: "/admin/settings", label: "Settings" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Horizontal admin nav. Consumers can extend the link set by overriding the
|
|
21
|
+
* `Navigation` slot if they need additional sections; the default set covers
|
|
22
|
+
* everything this package ships.
|
|
23
|
+
*/
|
|
24
|
+
export function AdminNav({ extraLinks }: { extraLinks?: NavLink[] }) {
|
|
25
|
+
const pathname = usePathname();
|
|
26
|
+
const links = [...DEFAULT_LINKS, ...(extraLinks ?? [])];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<nav className="gm-admin-nav flex items-center gap-4 overflow-x-auto border-b border-border pb-4 mb-6">
|
|
30
|
+
{links.map(({ href, label, exact }) => {
|
|
31
|
+
const active = exact
|
|
32
|
+
? pathname === href
|
|
33
|
+
: pathname === href || pathname.startsWith(`${href}/`);
|
|
34
|
+
return (
|
|
35
|
+
<Link
|
|
36
|
+
key={href}
|
|
37
|
+
href={href}
|
|
38
|
+
className={
|
|
39
|
+
active
|
|
40
|
+
? "text-sm font-semibold whitespace-nowrap"
|
|
41
|
+
: "text-sm text-muted-foreground hover:underline whitespace-nowrap"
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
{label}
|
|
45
|
+
</Link>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</nav>
|
|
49
|
+
);
|
|
50
|
+
}
|