Skip to content

Commit 39caf14

Browse files
aster-voidclaude
andcommitted
treewide: add DISABLE_AUTH feature flag and seed script
- Add DISABLE_AUTH env var to bypass authentication with mock user - Add PUBLIC_DISABLE_AUTH for frontend - Fix WebSocket message handling with proper Elysia body schema - Initialize WebSocket connection in root layout - Add scripts/seed.ts to seed mock user, org, and channel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 666fda7 commit 39caf14

File tree

9 files changed

+139
-29
lines changed

9 files changed

+139
-29
lines changed

.env.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ BETTER_AUTH_SECRET=your-secret-key-at-least-32-characters
99
# Google OAuth
1010
GOOGLE_CLIENT_ID=your-google-client-id
1111
GOOGLE_CLIENT_SECRET=your-google-client-secret
12+
# Feature Flags
13+
DISABLE_AUTH=false
14+
PUBLIC_DISABLE_AUTH=false

apps/desktop/src/lib/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import * as v from "valibot";
22

33
const envSchema = v.object({
44
PUBLIC_API_BASE_URL: v.pipe(v.string(), v.url()),
5+
PUBLIC_DISABLE_AUTH: v.optional(v.string(), "false"),
56
});
67

78
const result = v.safeParse(envSchema, {
89
PUBLIC_API_BASE_URL: import.meta.env.PUBLIC_API_BASE_URL,
10+
PUBLIC_DISABLE_AUTH: import.meta.env.PUBLIC_DISABLE_AUTH,
911
});
1012

1113
if (!result.success) {
@@ -16,3 +18,5 @@ if (!result.success) {
1618
}
1719

1820
export const env = result.output;
21+
22+
export const isAuthDisabled = () => env.PUBLIC_DISABLE_AUTH === "true";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
<script lang="ts">
22
import "@/app.css";
33
import { setupApi } from "@/lib/api.svelte.ts";
4+
import { env } from "@/lib/env.ts";
5+
import { setupWebSocket } from "@/lib/websocket/index.ts";
46
57
const { children } = $props();
68
79
// Initialize API client (uses PUBLIC_API_BASE_URL env var)
810
setupApi();
11+
12+
// Initialize WebSocket client
13+
const wsUrl = `${env.PUBLIC_API_BASE_URL.replace(/^http/, "ws")}/ws`;
14+
const ws = setupWebSocket(wsUrl);
15+
16+
$effect(() => {
17+
ws.connect();
18+
return () => ws.disconnect();
19+
});
920
</script>
1021

1122
{@render children()}

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"scripts": {
99
"dev": "bun run --watch src/index.ts",
1010
"check": "tsc --noEmit",
11-
"db:migrate": "drizzle-kit migrate"
11+
"db": "drizzle-kit"
1212
},
1313
"dependencies": {
1414
"@bogeychan/elysia-logger": "^0.1.10",

apps/server/src/env.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ const envSchema = v.object({
77
PORT: v.optional(v.pipe(v.string(), v.transform(Number)), "3000"),
88
GOOGLE_CLIENT_ID: v.pipe(v.string(), v.minLength(1)),
99
GOOGLE_CLIENT_SECRET: v.pipe(v.string(), v.minLength(1)),
10+
DISABLE_AUTH: v.optional(
11+
v.pipe(
12+
v.string(),
13+
v.transform((s) => s === "true"),
14+
),
15+
"false",
16+
),
1017
});
1118

1219
const result = v.safeParse(envSchema, process.env);

apps/server/src/middleware/auth.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { Elysia } from "elysia";
22
import { auth } from "../auth.ts";
3+
import { env } from "../env.ts";
34

45
export interface AuthUser {
56
id: string;
67
email: string;
78
name?: string;
89
}
910

11+
const MOCK_USER: AuthUser = {
12+
id: "dev-user-id",
13+
14+
name: "Dev User",
15+
};
16+
1017
export const authMiddleware = new Elysia({ name: "auth" }).derive(
1118
{ as: "global" },
1219
async ({ request }) => {
20+
if (env.DISABLE_AUTH) {
21+
return { user: MOCK_USER as AuthUser | null };
22+
}
23+
1324
const session = await auth.api.getSession({ headers: request.headers });
1425

1526
if (!session?.user) {

apps/server/src/ws/index.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import { Elysia } from "elysia";
1+
import { Elysia, t } from "elysia";
22
import { authMiddleware } from "../middleware/auth.ts";
33
import { wsManager } from "./manager.ts";
4-
import type { WsClientMessage, WsConnection } from "./types.ts";
4+
import type { WsConnection } from "./types.ts";
55

66
/**
77
* WebSocket routes for real-time communication.
88
* Handles channel subscriptions and broadcasts events to connected clients.
99
*/
10+
const wsClientMessage = t.Union([
11+
t.Object({ type: t.Literal("subscribe"), channelId: t.String() }),
12+
t.Object({ type: t.Literal("unsubscribe"), channelId: t.String() }),
13+
t.Object({ type: t.Literal("ping") }),
14+
]);
15+
1016
export const wsRoutes = new Elysia().use(authMiddleware).ws("/ws", {
17+
body: wsClientMessage,
18+
1119
open(ws) {
1220
const user = ws.data.user;
1321
if (!user) {
@@ -29,32 +37,22 @@ export const wsRoutes = new Elysia().use(authMiddleware).ws("/ws", {
2937
const user = ws.data.user;
3038
if (!user) return;
3139

32-
try {
33-
const data = JSON.parse(String(msg)) as WsClientMessage;
34-
35-
if (data.type === "subscribe") {
36-
const connections = Array.from(wsManager.connections.values());
37-
const conn = connections.find((c) => c.user.id === user.id);
38-
if (conn) {
39-
wsManager.subscribe(conn.id, data.channelId);
40-
ws.send(
41-
JSON.stringify({ type: "subscribed", channelId: data.channelId }),
42-
);
43-
}
44-
} else if (data.type === "unsubscribe") {
45-
const connections = Array.from(wsManager.connections.values());
46-
const conn = connections.find((c) => c.user.id === user.id);
47-
if (conn) {
48-
wsManager.unsubscribe(conn.id, data.channelId);
49-
ws.send(
50-
JSON.stringify({ type: "unsubscribed", channelId: data.channelId }),
51-
);
52-
}
53-
} else if (data.type === "ping") {
54-
ws.send(JSON.stringify({ type: "pong" }));
40+
if (msg.type === "subscribe") {
41+
const connections = Array.from(wsManager.connections.values());
42+
const conn = connections.find((c) => c.user.id === user.id);
43+
if (conn) {
44+
wsManager.subscribe(conn.id, msg.channelId);
45+
ws.send({ type: "subscribed", channelId: msg.channelId });
46+
}
47+
} else if (msg.type === "unsubscribe") {
48+
const connections = Array.from(wsManager.connections.values());
49+
const conn = connections.find((c) => c.user.id === user.id);
50+
if (conn) {
51+
wsManager.unsubscribe(conn.id, msg.channelId);
52+
ws.send({ type: "unsubscribed", channelId: msg.channelId });
5553
}
56-
} catch (error) {
57-
console.error("WebSocket message error:", error);
54+
} else if (msg.type === "ping") {
55+
ws.send({ type: "pong" });
5856
}
5957
},
6058

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"fix:format": "prettier --write . --log-level warn",
1616
"check:lint": "bun biome check",
1717
"fix:lint": "bun biome check --fix --unsafe",
18-
"db": "cd apps/server && bun --env-file=../../.env run drizzle-kit"
18+
"db": "cd apps/server && bun --env-file=../../.env run drizzle-kit",
19+
"db:seed": "bun --env-file=.env run scripts/seed.ts"
1920
},
2021
"devDependencies": {
2122
"@biomejs/biome": "^2.3.8",

scripts/seed.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { users } from "../apps/server/src/db/auth.ts";
2+
import { channels } from "../apps/server/src/db/channels.ts";
3+
import { db } from "../apps/server/src/db/index.ts";
4+
import {
5+
organizationMembers,
6+
organizations,
7+
} from "../apps/server/src/db/organizations.ts";
8+
9+
const MOCK_USER = {
10+
id: "dev-user-id",
11+
name: "Dev User",
12+
13+
};
14+
15+
const MOCK_ORG = {
16+
name: "Dev Organization",
17+
description: "Default organization for development",
18+
};
19+
20+
async function seed() {
21+
console.log("🌱 Seeding database...");
22+
23+
// Create mock user
24+
const [user] = await db
25+
.insert(users)
26+
.values(MOCK_USER)
27+
.onConflictDoNothing()
28+
.returning();
29+
30+
if (user) {
31+
console.log(`✅ Created user: ${user.name}`);
32+
} else {
33+
console.log(`⏭️ User already exists: ${MOCK_USER.name}`);
34+
}
35+
36+
// Create mock organization
37+
const [org] = await db
38+
.insert(organizations)
39+
.values({ ...MOCK_ORG, ownerId: MOCK_USER.id })
40+
.onConflictDoNothing()
41+
.returning();
42+
43+
if (org) {
44+
console.log(`✅ Created organization: ${org.name}`);
45+
46+
// Add user as admin member
47+
await db.insert(organizationMembers).values({
48+
organizationId: org.id,
49+
userId: MOCK_USER.id,
50+
permission: "admin",
51+
});
52+
console.log(`✅ Added ${MOCK_USER.name} as admin`);
53+
54+
// Create general channel
55+
const [channel] = await db
56+
.insert(channels)
57+
.values({
58+
name: "general",
59+
description: "General discussion",
60+
organizationId: org.id,
61+
})
62+
.returning();
63+
console.log(`✅ Created channel: #${channel.name}`);
64+
} else {
65+
console.log("⏭️ Organization already exists");
66+
}
67+
68+
console.log("🌱 Seeding complete!");
69+
process.exit(0);
70+
}
71+
72+
seed().catch((err) => {
73+
console.error("❌ Seeding failed:", err);
74+
process.exit(1);
75+
});

0 commit comments

Comments
 (0)