@sansavision/create-pulse 0.4.4 → 0.4.6
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/dist/index.js +2 -0
- package/package.json +2 -2
- package/templates/aurora-auth-node-demo/README.md +43 -0
- package/templates/aurora-auth-node-demo/aurora.config.ts +15 -0
- package/templates/aurora-auth-node-demo/bun.lock +679 -0
- package/templates/aurora-auth-node-demo/drizzle.config.ts +9 -0
- package/templates/aurora-auth-node-demo/package.json +39 -0
- package/templates/aurora-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/aurora-auth-node-demo/server.mjs +46 -0
- package/templates/aurora-auth-node-demo/src/actions/createMessage.action.server.ts +31 -0
- package/templates/aurora-auth-node-demo/src/aurora.auth.ts +65 -0
- package/templates/aurora-auth-node-demo/src/lib/auth-client.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.server.ts +11 -0
- package/templates/aurora-auth-node-demo/src/lib/auth.ts +30 -0
- package/templates/aurora-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/aurora-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/aurora-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/aurora-auth-node-demo/src/queries/listMessages.server.ts +25 -0
- package/templates/aurora-auth-node-demo/src/routes/api/auth/[...slug]/handler.ts +14 -0
- package/templates/aurora-auth-node-demo/src/routes/api/pulse/verify/handler.ts +55 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.client.tsx +132 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-in/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.client.tsx +154 -0
- package/templates/aurora-auth-node-demo/src/routes/auth/sign-up/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.client.tsx +640 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/arena-game/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.client.tsx +349 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.client.tsx +472 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/encrypted-chat/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.client.tsx +375 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/game-sync/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.client.tsx +423 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/queues/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.client.tsx +840 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/video-call/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.client.tsx +722 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/demos/watch-together/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.client.tsx +113 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/layout.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.client.tsx +195 -0
- package/templates/aurora-auth-node-demo/src/routes/dashboard/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/routes/favicon.ico +0 -0
- package/templates/aurora-auth-node-demo/src/routes/layout.tsx +18 -0
- package/templates/aurora-auth-node-demo/src/routes/page.client.tsx +263 -0
- package/templates/aurora-auth-node-demo/src/routes/page.tsx +5 -0
- package/templates/aurora-auth-node-demo/src/styles/app.css +96 -0
- package/templates/aurora-auth-node-demo/tsconfig.json +27 -0
- package/templates/aurora-auth-node-demo/tsconfig.tsbuildinfo +1 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +1 -1
- package/templates/nextjs-auth-demo/package.json +8 -7
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/arena-game/page.tsx +20 -3
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +108 -23
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +278 -217
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +66 -35
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +213 -87
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/video-call/page.tsx +106 -6
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +415 -262
- package/templates/nextjs-auth-node-demo/.env.example +10 -0
- package/templates/nextjs-auth-node-demo/Dockerfile +19 -0
- package/templates/nextjs-auth-node-demo/README.md +159 -0
- package/templates/nextjs-auth-node-demo/_gitignore +33 -0
- package/templates/nextjs-auth-node-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-node-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-node-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-node-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-node-demo/package.json +38 -0
- package/templates/nextjs-auth-node-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-node-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-node-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-node-demo/server.mjs +45 -0
- package/templates/nextjs-auth-node-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-node-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-node-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/arena-game/page.tsx +640 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/chat/page.tsx +349 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +472 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/game-sync/page.tsx +375 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/queues/page.tsx +423 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/video-call/page.tsx +840 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/demos/watch-together/page.tsx +724 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/layout.tsx +113 -0
- package/templates/nextjs-auth-node-demo/src/app/dashboard/page.tsx +195 -0
- package/templates/nextjs-auth-node-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-node-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-node-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-node-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-node-demo/src/lib/auth.ts +14 -0
- package/templates/nextjs-auth-node-demo/src/lib/db.ts +6 -0
- package/templates/nextjs-auth-node-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-node-demo/src/lib/schema.ts +107 -0
- package/templates/nextjs-auth-node-demo/tsconfig.json +34 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pulse-aurora-auth-node-demo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "NODE_ENV=development bun server.mjs",
|
|
8
|
+
"build": "aurora build --target node",
|
|
9
|
+
"start": "NODE_ENV=production bun server.mjs",
|
|
10
|
+
"doctor": "aurora doctor",
|
|
11
|
+
"db:push": "bunx drizzle-kit push",
|
|
12
|
+
"db:generate": "bunx drizzle-kit generate",
|
|
13
|
+
"db:migrate": "bunx drizzle-kit migrate",
|
|
14
|
+
"db:studio": "bunx drizzle-kit studio"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@sansavision/aurora": "0.1.0-alpha.20260307.5",
|
|
18
|
+
"@sansavision/pulse-node": "file:../../../pulse-node",
|
|
19
|
+
"@sansavision/pulse-sdk": "^0.4.2",
|
|
20
|
+
"better-auth": "^1.2.0",
|
|
21
|
+
"better-sqlite3": "^12.0.0",
|
|
22
|
+
"drizzle-orm": "^0.41.0",
|
|
23
|
+
"lucide-react": "^0.412.0",
|
|
24
|
+
"react": "19.2.3",
|
|
25
|
+
"react-dom": "19.2.3",
|
|
26
|
+
"react-player": "^2.16.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tailwindcss/cli": "^4.2.1",
|
|
30
|
+
"@tailwindcss/postcss": "^4",
|
|
31
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
32
|
+
"@types/node": "^20",
|
|
33
|
+
"@types/react": "^19",
|
|
34
|
+
"@types/react-dom": "^19",
|
|
35
|
+
"drizzle-kit": "^0.31.0",
|
|
36
|
+
"tailwindcss": "^4.2.1",
|
|
37
|
+
"typescript": "^5"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { PulseRelay } from "@sansavision/pulse-node";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
6
|
+
const hostname = process.env.HOST || "localhost";
|
|
7
|
+
const port = parseInt(process.env.PORT || "3000", 10);
|
|
8
|
+
const pulsePort = parseInt(process.env.PULSE_PORT || "4001", 10);
|
|
9
|
+
|
|
10
|
+
// ── Start the embedded Pulse relay ──────────────────────────────────────────
|
|
11
|
+
const relay = new PulseRelay({
|
|
12
|
+
bind: `0.0.0.0:${pulsePort}`,
|
|
13
|
+
enableWebsocket: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const info = await relay.start();
|
|
17
|
+
console.log(`⚡ Pulse Relay running on port ${info.wsPort || info.quicPort}`);
|
|
18
|
+
console.log(` Region: ${info.region} | Capacity: ${info.capacity}`);
|
|
19
|
+
|
|
20
|
+
// ── Start Web Server ────────────────────────────────────────────────────────
|
|
21
|
+
const args = dev ? ["run", "dev"] : ["run", "start"];
|
|
22
|
+
args.push("--port", port.toString());
|
|
23
|
+
|
|
24
|
+
const cp = spawn("bunx", ["aurora", ...args.slice(1)], {
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
env: process.env,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cp.on("close", (code) => {
|
|
30
|
+
console.log(`Aurora exited with code ${code}`);
|
|
31
|
+
shutdown();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── Graceful shutdown ───────────────────────────────────────────────────────
|
|
35
|
+
const shutdown = async () => {
|
|
36
|
+
console.log("\n🛑 Shutting down...");
|
|
37
|
+
await relay.stop();
|
|
38
|
+
if (cp.exitCode === null) {
|
|
39
|
+
cp.kill();
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
process.on("SIGTERM", shutdown);
|
|
45
|
+
process.on("SIGINT", shutdown);
|
|
46
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// aurora:justify-auth-none: template demo action; replace with real authentication (e.g. .auth("required")) before shipping.
|
|
2
|
+
import { err, ok, s, server } from "@sansavision/aurora";
|
|
3
|
+
|
|
4
|
+
import { requireUser } from "../lib/auth.server";
|
|
5
|
+
import { appendMessage } from "../queries/listMessages.server";
|
|
6
|
+
|
|
7
|
+
const createMessageInput = s.object({
|
|
8
|
+
text: s.string().trim().min(1).max(500),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const createMessage = server
|
|
12
|
+
.action("messages.create")
|
|
13
|
+
.input(createMessageInput)
|
|
14
|
+
.handler(async ({ input }: { input: { text: string } }) => {
|
|
15
|
+
if (input.text.length > 280) {
|
|
16
|
+
return err("MESSAGE_TOO_LONG", {
|
|
17
|
+
max: 280,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const user = await requireUser();
|
|
22
|
+
const record = appendMessage(input.text);
|
|
23
|
+
|
|
24
|
+
return ok({
|
|
25
|
+
...record,
|
|
26
|
+
authorId: user.id,
|
|
27
|
+
});
|
|
28
|
+
})
|
|
29
|
+
.invalidates(["messages"])
|
|
30
|
+
.auth("none")
|
|
31
|
+
.errors(["MESSAGE_TOO_LONG"] as const);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { defineAuthPipeline } from "@sansavision/aurora/server";
|
|
2
|
+
import { auth } from "@/lib/auth";
|
|
3
|
+
|
|
4
|
+
function normalizeAuroraRoles(role: string | undefined): string[] {
|
|
5
|
+
if (role === "game_master") {
|
|
6
|
+
return ["user", "admin"];
|
|
7
|
+
}
|
|
8
|
+
return ["user"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default defineAuthPipeline({
|
|
12
|
+
async resolve(request: Request) {
|
|
13
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
14
|
+
if (!cookieHeader) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const headers = new Headers();
|
|
19
|
+
headers.set("cookie", cookieHeader);
|
|
20
|
+
|
|
21
|
+
const session = await auth.api.getSession({ headers });
|
|
22
|
+
|
|
23
|
+
if (!session?.user?.id) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Better Auth's getSession returns the full user row — our custom
|
|
28
|
+
// `role` column is present at runtime but not in the default TS types.
|
|
29
|
+
const userRecord = session.user as typeof session.user & { role?: string };
|
|
30
|
+
const roles = normalizeAuroraRoles(userRecord.role);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
authenticated: true,
|
|
34
|
+
subjectId: session.user.id,
|
|
35
|
+
roles,
|
|
36
|
+
permissions: [],
|
|
37
|
+
provider: "better-auth",
|
|
38
|
+
claims: {
|
|
39
|
+
email: session.user.email,
|
|
40
|
+
name: session.user.name,
|
|
41
|
+
role: userRecord.role ?? "player",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
deny: {
|
|
46
|
+
redirect: "/auth/login",
|
|
47
|
+
},
|
|
48
|
+
routing: [
|
|
49
|
+
{
|
|
50
|
+
path: "/auth/login",
|
|
51
|
+
condition: "authenticated",
|
|
52
|
+
redirect: "/",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: "/lobbies",
|
|
56
|
+
condition: "unauthenticated",
|
|
57
|
+
redirect: "/auth/login",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: "/matches",
|
|
61
|
+
condition: "unauthenticated",
|
|
62
|
+
redirect: "/auth/login",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
|
+
// Aurora only dispatches handler.ts files for non-GET HTTP methods.
|
|
4
|
+
// better-auth uses GET for /get-session. We wrap fetch to convert
|
|
5
|
+
// those GETs into POSTs so they hit the handler instead of the page renderer.
|
|
6
|
+
const auroraFetch: typeof fetch = (input, init) => {
|
|
7
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
8
|
+
if (url.includes("/api/auth/") && (!init?.method || init.method === "GET")) {
|
|
9
|
+
return fetch(input, { ...init, method: "POST" });
|
|
10
|
+
}
|
|
11
|
+
return fetch(input, init);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const authClient = createAuthClient({
|
|
15
|
+
baseURL: typeof window !== "undefined" ? window.location.origin : "http://localhost:3000",
|
|
16
|
+
fetchOptions: {
|
|
17
|
+
customFetchImpl: auroraFetch,
|
|
18
|
+
onSuccess: (ctx) => {
|
|
19
|
+
const authToken = ctx.response.headers.get("set-auth-token");
|
|
20
|
+
if (authToken && typeof window !== "undefined") {
|
|
21
|
+
localStorage.setItem("pulse_bearer_token", authToken);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const signIn = authClient.signIn;
|
|
28
|
+
export const signUp = authClient.signUp;
|
|
29
|
+
export const signOut = authClient.signOut;
|
|
30
|
+
export const useSession = authClient.useSession;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { bearer } from "better-auth/plugins";
|
|
4
|
+
import { db } from "./db";
|
|
5
|
+
import * as schema from "./schema";
|
|
6
|
+
|
|
7
|
+
const baseURL =
|
|
8
|
+
process.env.BETTER_AUTH_URL ||
|
|
9
|
+
`http://localhost:${process.env.PORT || "3000"}`;
|
|
10
|
+
|
|
11
|
+
export const auth = betterAuth({
|
|
12
|
+
baseURL,
|
|
13
|
+
database: drizzleAdapter(db, { provider: "sqlite", schema }),
|
|
14
|
+
emailAndPassword: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
},
|
|
17
|
+
session: {
|
|
18
|
+
// Aurora only dispatches handler.ts for non-GET methods, so the
|
|
19
|
+
// client converts GET /get-session → POST. better-auth requires
|
|
20
|
+
// deferSessionRefresh to accept POST on the get-session endpoint.
|
|
21
|
+
deferSessionRefresh: true,
|
|
22
|
+
},
|
|
23
|
+
plugins: [bearer()],
|
|
24
|
+
// In production, set BETTER_AUTH_URL to your domain.
|
|
25
|
+
// In dev, we trust any localhost origin regardless of port.
|
|
26
|
+
trustedOrigins: (origin) => {
|
|
27
|
+
if (origin.startsWith("http://localhost:")) return true;
|
|
28
|
+
return origin === baseURL;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Pulse } from "@sansavision/pulse-sdk";
|
|
4
|
+
|
|
5
|
+
let pulseInstance: Pulse | null = null;
|
|
6
|
+
|
|
7
|
+
export function getPulse(): Pulse {
|
|
8
|
+
if (!pulseInstance) {
|
|
9
|
+
pulseInstance = new Pulse({ apiKey: "demo" });
|
|
10
|
+
}
|
|
11
|
+
return pulseInstance;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the bearer token stored by Better Auth for Pulse auth.
|
|
16
|
+
*/
|
|
17
|
+
export function getPulseToken(): string | null {
|
|
18
|
+
if (typeof window === "undefined") return null;
|
|
19
|
+
return localStorage.getItem("pulse_bearer_token");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Connect to the Pulse relay with authentication.
|
|
24
|
+
* Uses the Better Auth bearer token for the PLP CONNECT handshake.
|
|
25
|
+
*/
|
|
26
|
+
export async function connectWithAuth() {
|
|
27
|
+
const pulse = getPulse();
|
|
28
|
+
const token = getPulseToken();
|
|
29
|
+
const url = process.env.NEXT_PUBLIC_PULSE_URL || "ws://localhost:4001";
|
|
30
|
+
|
|
31
|
+
const conn = await pulse.connect(url, {
|
|
32
|
+
encoding: "json",
|
|
33
|
+
autoReconnect: true,
|
|
34
|
+
...(token
|
|
35
|
+
? {
|
|
36
|
+
auth: {
|
|
37
|
+
token,
|
|
38
|
+
provider: "better-auth",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return conn;
|
|
45
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { relations, sql } from "drizzle-orm";
|
|
2
|
+
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
|
3
|
+
|
|
4
|
+
export const user = sqliteTable("user", {
|
|
5
|
+
id: text("id").primaryKey(),
|
|
6
|
+
name: text("name").notNull(),
|
|
7
|
+
email: text("email").notNull().unique(),
|
|
8
|
+
emailVerified: integer("email_verified", { mode: "boolean" })
|
|
9
|
+
.default(false)
|
|
10
|
+
.notNull(),
|
|
11
|
+
image: text("image"),
|
|
12
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
13
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
14
|
+
.notNull(),
|
|
15
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
16
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
17
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
18
|
+
.notNull(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const session = sqliteTable(
|
|
22
|
+
"session",
|
|
23
|
+
{
|
|
24
|
+
id: text("id").primaryKey(),
|
|
25
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
26
|
+
token: text("token").notNull().unique(),
|
|
27
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
28
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
29
|
+
.notNull(),
|
|
30
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
31
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
32
|
+
.notNull(),
|
|
33
|
+
ipAddress: text("ip_address"),
|
|
34
|
+
userAgent: text("user_agent"),
|
|
35
|
+
userId: text("user_id")
|
|
36
|
+
.notNull()
|
|
37
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
38
|
+
},
|
|
39
|
+
(table) => [index("session_userId_idx").on(table.userId)],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const account = sqliteTable(
|
|
43
|
+
"account",
|
|
44
|
+
{
|
|
45
|
+
id: text("id").primaryKey(),
|
|
46
|
+
accountId: text("account_id").notNull(),
|
|
47
|
+
providerId: text("provider_id").notNull(),
|
|
48
|
+
userId: text("user_id")
|
|
49
|
+
.notNull()
|
|
50
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
51
|
+
accessToken: text("access_token"),
|
|
52
|
+
refreshToken: text("refresh_token"),
|
|
53
|
+
idToken: text("id_token"),
|
|
54
|
+
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
55
|
+
mode: "timestamp_ms",
|
|
56
|
+
}),
|
|
57
|
+
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
|
58
|
+
mode: "timestamp_ms",
|
|
59
|
+
}),
|
|
60
|
+
scope: text("scope"),
|
|
61
|
+
password: text("password"),
|
|
62
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
63
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
64
|
+
.notNull(),
|
|
65
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
66
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
67
|
+
.notNull(),
|
|
68
|
+
},
|
|
69
|
+
(table) => [index("account_userId_idx").on(table.userId)],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const verification = sqliteTable(
|
|
73
|
+
"verification",
|
|
74
|
+
{
|
|
75
|
+
id: text("id").primaryKey(),
|
|
76
|
+
identifier: text("identifier").notNull(),
|
|
77
|
+
value: text("value").notNull(),
|
|
78
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
79
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
80
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
81
|
+
.notNull(),
|
|
82
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
83
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
84
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
85
|
+
.notNull(),
|
|
86
|
+
},
|
|
87
|
+
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const userRelations = relations(user, ({ many }) => ({
|
|
91
|
+
sessions: many(session),
|
|
92
|
+
accounts: many(account),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
export const sessionRelations = relations(session, ({ one }) => ({
|
|
96
|
+
user: one(user, {
|
|
97
|
+
fields: [session.userId],
|
|
98
|
+
references: [user.id],
|
|
99
|
+
}),
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
export const accountRelations = relations(account, ({ one }) => ({
|
|
103
|
+
user: one(user, {
|
|
104
|
+
fields: [account.userId],
|
|
105
|
+
references: [user.id],
|
|
106
|
+
}),
|
|
107
|
+
}));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { server } from "@sansavision/aurora";
|
|
2
|
+
|
|
3
|
+
export interface MessageListItem {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const messages: MessageListItem[] = [];
|
|
10
|
+
|
|
11
|
+
export function appendMessage(text: string): MessageListItem {
|
|
12
|
+
const record: MessageListItem = {
|
|
13
|
+
id: `msg-${Date.now()}`,
|
|
14
|
+
text,
|
|
15
|
+
createdAt: Date.now(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
messages.unshift(record);
|
|
19
|
+
return record;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const listMessages = server
|
|
23
|
+
.query("messages.list", async () => messages)
|
|
24
|
+
.key(() => ["messages", "list"])
|
|
25
|
+
.tags(() => ["messages"]);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { auth } from "@/lib/auth";
|
|
2
|
+
|
|
3
|
+
// Aurora handler resolution requires a default export.
|
|
4
|
+
// Aurora may pass either a raw Request or a context object { request, params }.
|
|
5
|
+
// We handle both cases.
|
|
6
|
+
export default async function handler(input: Request | { request: Request; params?: Record<string, string> }) {
|
|
7
|
+
const request = input instanceof Request ? input : input.request;
|
|
8
|
+
try {
|
|
9
|
+
return await auth.handler(request);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error("[auth handler] Error:", error);
|
|
12
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { auth } from "@/lib/auth";
|
|
2
|
+
|
|
3
|
+
// Aurora handler resolution requires a default export.
|
|
4
|
+
// Aurora may pass either a raw Request or a context object { request, params }.
|
|
5
|
+
export default async function handler(input: Request | { request: Request; params?: Record<string, string> }) {
|
|
6
|
+
const request = input instanceof Request ? input : input.request;
|
|
7
|
+
|
|
8
|
+
if (request.method !== "POST") {
|
|
9
|
+
return Response.json(
|
|
10
|
+
{ error: "Method not allowed" },
|
|
11
|
+
{ status: 405 }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.json();
|
|
17
|
+
const { token } = body;
|
|
18
|
+
|
|
19
|
+
if (!token) {
|
|
20
|
+
return Response.json(
|
|
21
|
+
{ allow: false, reason: "No token provided" },
|
|
22
|
+
{ status: 200 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Verify the bearer token against Better Auth's session store
|
|
27
|
+
const session = await auth.api.getSession({
|
|
28
|
+
headers: new Headers({
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!session?.user) {
|
|
34
|
+
return Response.json(
|
|
35
|
+
{ allow: false, reason: "Invalid or expired session token" },
|
|
36
|
+
{ status: 200 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Response.json({
|
|
41
|
+
allow: true,
|
|
42
|
+
user_id: session.user.id,
|
|
43
|
+
claims: {
|
|
44
|
+
email: session.user.email || "",
|
|
45
|
+
name: session.user.name || "",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("[Pulse Verify] Error:", error);
|
|
50
|
+
return Response.json(
|
|
51
|
+
{ allow: false, reason: "Internal verification error" },
|
|
52
|
+
{ status: 200 }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { createClientNavigation } from "@sansavision/aurora/router";
|
|
5
|
+
|
|
6
|
+
import { signIn } from "@/lib/auth-client";
|
|
7
|
+
import { Zap, Mail, Lock, ArrowRight, Loader2 } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
export default function SignInPage() {
|
|
10
|
+
const router = createClientNavigation();
|
|
11
|
+
const [email, setEmail] = useState("");
|
|
12
|
+
const [password, setPassword] = useState("");
|
|
13
|
+
const [error, setError] = useState("");
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setError("");
|
|
19
|
+
setLoading(true);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const result = await signIn.email({
|
|
23
|
+
email,
|
|
24
|
+
password,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (result.error) {
|
|
28
|
+
setError(result.error.message || "Sign in failed");
|
|
29
|
+
} else {
|
|
30
|
+
router.navigate("/dashboard");
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
setError("An unexpected error occurred");
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
|
|
41
|
+
{/* Background effects */}
|
|
42
|
+
<div className="absolute top-1/4 left-1/3 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px]" />
|
|
43
|
+
<div className="absolute bottom-1/4 right-1/3 w-80 h-80 bg-cyan-500/15 rounded-full blur-[100px]" />
|
|
44
|
+
|
|
45
|
+
<div className="w-full max-w-md relative z-10">
|
|
46
|
+
{/* Logo */}
|
|
47
|
+
<div className="text-center mb-8">
|
|
48
|
+
<a href="/" className="inline-flex items-center gap-2 mb-6">
|
|
49
|
+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-cyan-500 flex items-center justify-center">
|
|
50
|
+
<Zap className="w-6 h-6 text-white" />
|
|
51
|
+
</div>
|
|
52
|
+
<span className="text-2xl font-bold">Pulse</span>
|
|
53
|
+
</a>
|
|
54
|
+
<h1 className="text-2xl font-bold mb-2">Welcome back</h1>
|
|
55
|
+
<p className="text-slate-400">Sign in to access the demo dashboard</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Form */}
|
|
59
|
+
<form
|
|
60
|
+
method="post"
|
|
61
|
+
onSubmit={handleSubmit}
|
|
62
|
+
className="glass rounded-2xl p-8 space-y-5"
|
|
63
|
+
>
|
|
64
|
+
{error && (
|
|
65
|
+
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
|
66
|
+
{error}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<div>
|
|
71
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
72
|
+
Email
|
|
73
|
+
</label>
|
|
74
|
+
<div className="relative">
|
|
75
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
76
|
+
<input
|
|
77
|
+
type="email"
|
|
78
|
+
value={email}
|
|
79
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
80
|
+
placeholder="you@example.com"
|
|
81
|
+
required
|
|
82
|
+
className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div>
|
|
88
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
89
|
+
Password
|
|
90
|
+
</label>
|
|
91
|
+
<div className="relative">
|
|
92
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
93
|
+
<input
|
|
94
|
+
type="password"
|
|
95
|
+
value={password}
|
|
96
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
97
|
+
placeholder="••••••••"
|
|
98
|
+
required
|
|
99
|
+
className="w-full pl-10 pr-4 py-2.5 rounded-lg bg-slate-800/50 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none text-sm transition-colors placeholder:text-slate-600"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<button
|
|
105
|
+
type="submit"
|
|
106
|
+
disabled={loading}
|
|
107
|
+
className="w-full py-2.5 bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 rounded-lg font-semibold transition-all hover:shadow-lg hover:shadow-purple-500/25 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
108
|
+
>
|
|
109
|
+
{loading ? (
|
|
110
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
111
|
+
) : (
|
|
112
|
+
<>
|
|
113
|
+
Sign In
|
|
114
|
+
<ArrowRight className="w-4 h-4" />
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</button>
|
|
118
|
+
|
|
119
|
+
<p className="text-center text-sm text-slate-400">
|
|
120
|
+
Don't have an account?{" "}
|
|
121
|
+
<a
|
|
122
|
+
href="/auth/sign-up"
|
|
123
|
+
className="text-purple-400 hover:text-purple-300 font-medium"
|
|
124
|
+
>
|
|
125
|
+
Create one
|
|
126
|
+
</a>
|
|
127
|
+
</p>
|
|
128
|
+
</form>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|