@sansavision/create-pulse 0.4.0 → 0.4.2
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 +27 -3
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/templates/nextjs-auth-demo/.env.example +6 -0
- package/templates/nextjs-auth-demo/README.md +74 -0
- package/templates/nextjs-auth-demo/_gitignore +33 -0
- package/templates/nextjs-auth-demo/drizzle.config.ts +10 -0
- package/templates/nextjs-auth-demo/eslint.config.mjs +18 -0
- package/templates/nextjs-auth-demo/next-env.d.ts +6 -0
- package/templates/nextjs-auth-demo/next.config.ts +7 -0
- package/templates/nextjs-auth-demo/package.json +34 -0
- package/templates/nextjs-auth-demo/postcss.config.mjs +7 -0
- package/templates/nextjs-auth-demo/public/file.svg +1 -0
- package/templates/nextjs-auth-demo/public/globe.svg +1 -0
- package/templates/nextjs-auth-demo/public/next.svg +1 -0
- package/templates/nextjs-auth-demo/public/vercel.svg +1 -0
- package/templates/nextjs-auth-demo/public/window.svg +1 -0
- package/templates/nextjs-auth-demo/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/nextjs-auth-demo/src/app/api/pulse/verify/route.ts +54 -0
- package/templates/nextjs-auth-demo/src/app/auth/sign-in/page.tsx +131 -0
- package/templates/nextjs-auth-demo/src/app/auth/sign-up/page.tsx +153 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/chat/page.tsx +248 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/encrypted-chat/page.tsx +198 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/game-sync/page.tsx +192 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/queues/page.tsx +297 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/demos/watch-together/page.tsx +258 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/layout.tsx +109 -0
- package/templates/nextjs-auth-demo/src/app/dashboard/page.tsx +147 -0
- package/templates/nextjs-auth-demo/src/app/favicon.ico +0 -0
- package/templates/nextjs-auth-demo/src/app/globals.css +96 -0
- package/templates/nextjs-auth-demo/src/app/layout.tsx +27 -0
- package/templates/nextjs-auth-demo/src/app/page.tsx +254 -0
- package/templates/nextjs-auth-demo/src/lib/auth-client.ts +15 -0
- package/templates/nextjs-auth-demo/src/lib/auth.ts +13 -0
- package/templates/nextjs-auth-demo/src/lib/db.ts +5 -0
- package/templates/nextjs-auth-demo/src/lib/pulse.ts +45 -0
- package/templates/nextjs-auth-demo/tsconfig.json +34 -0
- package/templates/react-all-features/package.json +2 -2
- package/templates/react-all-features/src/App.tsx +20 -39
- package/templates/react-all-features/src/components/EncryptedChat.tsx +8 -8
- package/templates/react-all-features/src/components/GameSync.tsx +38 -23
- package/templates/react-all-features/src/components/ServerMetrics.tsx +20 -15
- package/templates/react-queue-demo/README.md +6 -7
- package/templates/react-queue-demo/package.json +1 -1
- package/templates/react-queue-demo/src/App.tsx +229 -62
- package/templates/react-watch-together/package.json +2 -2
- package/templates/react-watch-together/src/App.tsx +18 -40
- package/src/index.ts +0 -115
package/README.md
CHANGED
|
@@ -26,18 +26,37 @@ npx @sansavision/create-pulse my-pulse-app
|
|
|
26
26
|
|
|
27
27
|
When you run `create-pulse`, you'll be prompted to select a template.
|
|
28
28
|
|
|
29
|
-
### 1.
|
|
29
|
+
### 1. Next.js + Auth (Full Demo) ⭐
|
|
30
|
+
An **investor-ready** demo application showcasing the full capabilities of Pulse.
|
|
31
|
+
- **Better Auth** with email/password, local SQLite + Drizzle ORM
|
|
32
|
+
- **Webhook auth** — Pulse relay verifies tokens via your Next.js API
|
|
33
|
+
- 5 comprehensive demos: Real-time Chat, Watch Together, Durable Queues (with offline simulation), Game State Sync, E2E Encrypted Chat
|
|
34
|
+
- Multi-user testing with separate browser tabs/incognito windows
|
|
35
|
+
- Built with **Next.js 16**, **React 19**, **Tailwind CSS v4**, **TypeScript**
|
|
36
|
+
|
|
37
|
+
### 2. Watch Together (React + TS)
|
|
30
38
|
A clean implementation of the "Watch Together" synchronized video player.
|
|
31
39
|
- Features a custom `usePulse` hook for lifecycle management.
|
|
32
40
|
- Handles syncing `play`, `pause`, and `seek` events across clients natively via the Pulse Relay.
|
|
33
41
|
- Built with React, Vite, Tailwind CSS, and TypeScript.
|
|
34
42
|
|
|
35
|
-
###
|
|
43
|
+
### 3. All Features (React + TS)
|
|
36
44
|
A kitchen-sink boilerplate that integrates multiple features.
|
|
37
45
|
- Perfect for exploring the SDK limits.
|
|
38
46
|
- Contains stubs and route setups for Chat, Metrics, and Video sync.
|
|
39
47
|
- Built with React, Vite, Tailwind CSS, and TypeScript.
|
|
40
48
|
|
|
49
|
+
### 4. Durable Queues (React + TS)
|
|
50
|
+
Demonstrates persistent message queues with publish/consume/ack workflows.
|
|
51
|
+
- Supports multiple storage backends (Memory, WAL, Postgres, Redis).
|
|
52
|
+
- Shows offline resilience and message recovery.
|
|
53
|
+
- Built with React, Vite, Tailwind CSS, and TypeScript.
|
|
54
|
+
|
|
55
|
+
### 5. Vanilla JS (Basic)
|
|
56
|
+
A minimal setup with zero frameworks.
|
|
57
|
+
- Raw WebSocket connection to the Pulse Relay.
|
|
58
|
+
- Good for understanding the protocol at a low level.
|
|
59
|
+
|
|
41
60
|
## Running Your Scaffolded App
|
|
42
61
|
|
|
43
62
|
After your app is generated:
|
|
@@ -55,4 +74,9 @@ After your app is generated:
|
|
|
55
74
|
npm run dev
|
|
56
75
|
```
|
|
57
76
|
|
|
58
|
-
**Note:** The templates are configured by default to connect to a local Pulse Relay running at `ws://localhost:4001`.
|
|
77
|
+
**Note:** The templates are configured by default to connect to a local Pulse Relay running at `ws://localhost:4001`. Start the server with `pulse serve` (or `pulse dev` for the full dev environment), or update your config to point to a production relay URL.
|
|
78
|
+
|
|
79
|
+
For the **Next.js + Auth** template, also start the relay with webhook auth:
|
|
80
|
+
```bash
|
|
81
|
+
npx @sansavision/pulse-cli serve --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
|
|
82
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ async function main() {
|
|
|
51
51
|
return p.select({
|
|
52
52
|
message: "Pick a template:",
|
|
53
53
|
options: [
|
|
54
|
+
{ value: "nextjs-auth-demo", label: "Next.js + Auth (Full Demo)", hint: "Better Auth, all features, investor-ready" },
|
|
54
55
|
{ value: "react-watch-together", label: "Watch Together (React + TS)", hint: "Synchronized video playback" },
|
|
55
56
|
{ value: "react-all-features", label: "All Features (React + TS)", hint: "Chat, Video, Audio, RPC" },
|
|
56
57
|
{ value: "react-queue-demo", label: "Durable Queues (React + TS)", hint: "Persistent queues with WAL/Postgres/Redis" },
|
|
@@ -101,7 +102,8 @@ function copyDir(src, dest) {
|
|
|
101
102
|
const entries = import_fs.default.readdirSync(src, { withFileTypes: true });
|
|
102
103
|
for (const entry of entries) {
|
|
103
104
|
const srcPath = import_path.default.join(src, entry.name);
|
|
104
|
-
if (
|
|
105
|
+
if (["node_modules", "dist", ".next", ".env", "package-lock.json"].includes(entry.name)) continue;
|
|
106
|
+
if (entry.name.endsWith(".db") || entry.name.endsWith(".db-journal")) continue;
|
|
105
107
|
const destPath = import_path.default.join(dest, entry.name);
|
|
106
108
|
if (entry.isDirectory()) {
|
|
107
109
|
if (!import_fs.default.existsSync(destPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sansavision/create-pulse",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Scaffold a new Pulse application",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"tsup": "^8.0.2",
|
|
30
30
|
"typescript": "^5.0.0"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Pulse + Next.js Auth Demo
|
|
2
|
+
|
|
3
|
+
An **investor-ready** demo application showcasing the full capabilities of [Pulse](https://github.com/Sansa-Organisation/pulse) — the real-time protocol for modern applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 **Better Auth** — Full authentication with email/password, local SQLite + Drizzle ORM
|
|
8
|
+
- 💬 **Real-time Chat** — Multi-user rooms with presence and typing indicators
|
|
9
|
+
- 📹 **Watch Together** — Synchronized video playback across users
|
|
10
|
+
- 📬 **Durable Queues** — Offline simulation with message persistence
|
|
11
|
+
- 🎮 **Game State Sync** — Real-time tic-tac-toe with shared state
|
|
12
|
+
- 🔒 **E2E Encrypted Chat** — End-to-end encryption with ciphertext visualization
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 1. Install dependencies
|
|
18
|
+
npm install
|
|
19
|
+
|
|
20
|
+
# 2. Create .env file
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
|
|
23
|
+
# 3. Generate a secret and update .env
|
|
24
|
+
openssl rand -base64 32
|
|
25
|
+
|
|
26
|
+
# 4. Create database tables
|
|
27
|
+
npx @auth/cli migrate
|
|
28
|
+
|
|
29
|
+
# 5. Start the Pulse relay (in another terminal)
|
|
30
|
+
npx @sansavision/pulse-cli serve --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
|
|
31
|
+
|
|
32
|
+
# 6. Start the app
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Multi-User Testing
|
|
37
|
+
|
|
38
|
+
1. Open `http://localhost:3000` in a browser
|
|
39
|
+
2. Create an account and sign in
|
|
40
|
+
3. Open a second browser tab (or incognito window)
|
|
41
|
+
4. Create a different account
|
|
42
|
+
5. Both users can now interact in real-time across all demos
|
|
43
|
+
|
|
44
|
+
## Architecture
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Next.js App (port 3000) Pulse Relay (port 4001)
|
|
48
|
+
├── /api/auth/[...all] ◄──────┐ ├── WebSocket gateway
|
|
49
|
+
├── /api/pulse/verify ◄──────┼──── ├── Auth webhook verification
|
|
50
|
+
├── / (Landing) │ └── QUIC streams
|
|
51
|
+
├── /auth/sign-in │
|
|
52
|
+
├── /auth/sign-up │
|
|
53
|
+
└── /dashboard (protected) │
|
|
54
|
+
├── /demos/chat │
|
|
55
|
+
├── /demos/watch-together │
|
|
56
|
+
├── /demos/queues ─┘
|
|
57
|
+
├── /demos/game-sync
|
|
58
|
+
└── /demos/encrypted-chat
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Technologies
|
|
62
|
+
|
|
63
|
+
- **Next.js 16** + **React 19** + **Tailwind CSS v4**
|
|
64
|
+
- **Better Auth** with **bearer plugin**
|
|
65
|
+
- **SQLite** + **Drizzle ORM**
|
|
66
|
+
- **Pulse SDK** (`@sansavision/pulse-sdk`)
|
|
67
|
+
|
|
68
|
+
## Environment Variables
|
|
69
|
+
|
|
70
|
+
| Variable | Description | Default |
|
|
71
|
+
|----------|-------------|---------|
|
|
72
|
+
| `BETTER_AUTH_SECRET` | Auth encryption secret (32+ chars) | — |
|
|
73
|
+
| `BETTER_AUTH_URL` | Base URL of the app | `http://localhost:3000` |
|
|
74
|
+
| `NEXT_PUBLIC_PULSE_URL` | Pulse relay WebSocket URL | `ws://localhost:4001` |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnp
|
|
4
|
+
.pnp.js
|
|
5
|
+
|
|
6
|
+
# builds
|
|
7
|
+
.next/
|
|
8
|
+
out/
|
|
9
|
+
|
|
10
|
+
# database
|
|
11
|
+
*.db
|
|
12
|
+
*.db-journal
|
|
13
|
+
|
|
14
|
+
# misc
|
|
15
|
+
.DS_Store
|
|
16
|
+
*.pem
|
|
17
|
+
|
|
18
|
+
# debug
|
|
19
|
+
npm-debug.log*
|
|
20
|
+
yarn-debug.log*
|
|
21
|
+
yarn-error.log*
|
|
22
|
+
.pnpm-debug.log*
|
|
23
|
+
|
|
24
|
+
# env files
|
|
25
|
+
.env
|
|
26
|
+
.env*.local
|
|
27
|
+
|
|
28
|
+
# vercel
|
|
29
|
+
.vercel
|
|
30
|
+
|
|
31
|
+
# typescript
|
|
32
|
+
*.tsbuildinfo
|
|
33
|
+
next-env.d.ts
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pulse-nextjs-auth-demo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"db:push": "npx drizzle-kit push",
|
|
10
|
+
"db:studio": "npx drizzle-kit studio"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "16.1.6",
|
|
14
|
+
"react": "19.2.3",
|
|
15
|
+
"react-dom": "19.2.3",
|
|
16
|
+
"better-auth": "^1.2.0",
|
|
17
|
+
"better-sqlite3": "^12.0.0",
|
|
18
|
+
"drizzle-orm": "^0.41.0",
|
|
19
|
+
"@sansavision/pulse-sdk": "^0.4.2",
|
|
20
|
+
"lucide-react": "^0.412.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@tailwindcss/postcss": "^4",
|
|
24
|
+
"@types/node": "^20",
|
|
25
|
+
"@types/react": "^19",
|
|
26
|
+
"@types/react-dom": "^19",
|
|
27
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
28
|
+
"drizzle-kit": "^0.31.0",
|
|
29
|
+
"eslint": "^9",
|
|
30
|
+
"eslint-config-next": "16.1.6",
|
|
31
|
+
"tailwindcss": "^4",
|
|
32
|
+
"typescript": "^5"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { auth } from "@/lib/auth";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pulse Relay Auth Webhook
|
|
6
|
+
*
|
|
7
|
+
* The Pulse relay calls this endpoint during the CONNECT handshake
|
|
8
|
+
* to verify the client's auth token. We use Better Auth's session
|
|
9
|
+
* API to validate the bearer token and return user identity.
|
|
10
|
+
*
|
|
11
|
+
* Relay config: --auth-mode webhook --auth-webhook http://localhost:3000/api/pulse/verify
|
|
12
|
+
*/
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const { token } = body;
|
|
17
|
+
|
|
18
|
+
if (!token) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ allow: false, reason: "No token provided" },
|
|
21
|
+
{ status: 200 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Verify the bearer token against Better Auth's session store
|
|
26
|
+
const session = await auth.api.getSession({
|
|
27
|
+
headers: new Headers({
|
|
28
|
+
Authorization: `Bearer ${token}`,
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!session?.user) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ allow: false, reason: "Invalid or expired session token" },
|
|
35
|
+
{ status: 200 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({
|
|
40
|
+
allow: true,
|
|
41
|
+
user_id: session.user.id,
|
|
42
|
+
claims: {
|
|
43
|
+
email: session.user.email || "",
|
|
44
|
+
name: session.user.name || "",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("[Pulse Verify] Error:", error);
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ allow: false, reason: "Internal verification error" },
|
|
51
|
+
{ status: 200 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
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 = useRouter();
|
|
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.push("/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
|
+
<Link 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
|
+
</Link>
|
|
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
|
+
onSubmit={handleSubmit}
|
|
61
|
+
className="glass rounded-2xl p-8 space-y-5"
|
|
62
|
+
>
|
|
63
|
+
{error && (
|
|
64
|
+
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
|
65
|
+
{error}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<div>
|
|
70
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
71
|
+
Email
|
|
72
|
+
</label>
|
|
73
|
+
<div className="relative">
|
|
74
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
75
|
+
<input
|
|
76
|
+
type="email"
|
|
77
|
+
value={email}
|
|
78
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
79
|
+
placeholder="you@example.com"
|
|
80
|
+
required
|
|
81
|
+
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"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div>
|
|
87
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
88
|
+
Password
|
|
89
|
+
</label>
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
92
|
+
<input
|
|
93
|
+
type="password"
|
|
94
|
+
value={password}
|
|
95
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
96
|
+
placeholder="••••••••"
|
|
97
|
+
required
|
|
98
|
+
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"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<button
|
|
104
|
+
type="submit"
|
|
105
|
+
disabled={loading}
|
|
106
|
+
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"
|
|
107
|
+
>
|
|
108
|
+
{loading ? (
|
|
109
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
110
|
+
) : (
|
|
111
|
+
<>
|
|
112
|
+
Sign In
|
|
113
|
+
<ArrowRight className="w-4 h-4" />
|
|
114
|
+
</>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<p className="text-center text-sm text-slate-400">
|
|
119
|
+
Don't have an account?{" "}
|
|
120
|
+
<Link
|
|
121
|
+
href="/auth/sign-up"
|
|
122
|
+
className="text-purple-400 hover:text-purple-300 font-medium"
|
|
123
|
+
>
|
|
124
|
+
Create one
|
|
125
|
+
</Link>
|
|
126
|
+
</p>
|
|
127
|
+
</form>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { signUp } from "@/lib/auth-client";
|
|
7
|
+
import { Zap, Mail, Lock, User, ArrowRight, Loader2 } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
export default function SignUpPage() {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const [name, setName] = useState("");
|
|
12
|
+
const [email, setEmail] = useState("");
|
|
13
|
+
const [password, setPassword] = useState("");
|
|
14
|
+
const [error, setError] = useState("");
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
|
|
17
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setError("");
|
|
20
|
+
setLoading(true);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = await signUp.email({
|
|
24
|
+
name,
|
|
25
|
+
email,
|
|
26
|
+
password,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (result.error) {
|
|
30
|
+
setError(result.error.message || "Sign up failed");
|
|
31
|
+
} else {
|
|
32
|
+
router.push("/dashboard");
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
setError("An unexpected error occurred");
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
|
|
43
|
+
<div className="absolute top-1/4 left-1/3 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px]" />
|
|
44
|
+
<div className="absolute bottom-1/4 right-1/3 w-80 h-80 bg-cyan-500/15 rounded-full blur-[100px]" />
|
|
45
|
+
|
|
46
|
+
<div className="w-full max-w-md relative z-10">
|
|
47
|
+
<div className="text-center mb-8">
|
|
48
|
+
<Link 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
|
+
</Link>
|
|
54
|
+
<h1 className="text-2xl font-bold mb-2">Create your account</h1>
|
|
55
|
+
<p className="text-slate-400">
|
|
56
|
+
Start exploring the real-time protocol
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<form
|
|
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
|
+
Name
|
|
73
|
+
</label>
|
|
74
|
+
<div className="relative">
|
|
75
|
+
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
76
|
+
<input
|
|
77
|
+
type="text"
|
|
78
|
+
value={name}
|
|
79
|
+
onChange={(e) => setName(e.target.value)}
|
|
80
|
+
placeholder="Your name"
|
|
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
|
+
Email
|
|
90
|
+
</label>
|
|
91
|
+
<div className="relative">
|
|
92
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
93
|
+
<input
|
|
94
|
+
type="email"
|
|
95
|
+
value={email}
|
|
96
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
97
|
+
placeholder="you@example.com"
|
|
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
|
+
<div>
|
|
105
|
+
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
|
106
|
+
Password
|
|
107
|
+
</label>
|
|
108
|
+
<div className="relative">
|
|
109
|
+
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
|
110
|
+
<input
|
|
111
|
+
type="password"
|
|
112
|
+
value={password}
|
|
113
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
114
|
+
placeholder="••••••••"
|
|
115
|
+
required
|
|
116
|
+
minLength={8}
|
|
117
|
+
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"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
<p className="text-xs text-slate-500 mt-1">
|
|
121
|
+
Must be at least 8 characters
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<button
|
|
126
|
+
type="submit"
|
|
127
|
+
disabled={loading}
|
|
128
|
+
className="w-full py-2.5 bg-gradient-to-r from-purple-600 to-cyan-600 hover:from-purple-500 hover:to-cyan-500 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"
|
|
129
|
+
>
|
|
130
|
+
{loading ? (
|
|
131
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
132
|
+
) : (
|
|
133
|
+
<>
|
|
134
|
+
Create Account
|
|
135
|
+
<ArrowRight className="w-4 h-4" />
|
|
136
|
+
</>
|
|
137
|
+
)}
|
|
138
|
+
</button>
|
|
139
|
+
|
|
140
|
+
<p className="text-center text-sm text-slate-400">
|
|
141
|
+
Already have an account?{" "}
|
|
142
|
+
<Link
|
|
143
|
+
href="/auth/sign-in"
|
|
144
|
+
className="text-purple-400 hover:text-purple-300 font-medium"
|
|
145
|
+
>
|
|
146
|
+
Sign in
|
|
147
|
+
</Link>
|
|
148
|
+
</p>
|
|
149
|
+
</form>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|