Skip to content

Commit 1c0e11f

Browse files
authored
feat: add livekit routes (#6)
1 parent 5f61092 commit 1c0e11f

26 files changed

Lines changed: 1228 additions & 51 deletions

apps/server/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ NODE_ENV="development"
22
DATABASE_URL="postgres://verse:verse_secret@localhost:5432/verse_db"
33
LOG_LEVEL="debug"
44
JWT_SECRET="secret"
5+
6+
LIVEKIT_HOST="localhost:7880"
7+
LIVEKIT_API_KEY="your_api_key"
8+
LIVEKIT_SECRET_KEY="your_secret_key"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
CREATE TABLE "participants" (
2+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3+
"room_id" uuid NOT NULL,
4+
"user_id" uuid NOT NULL,
5+
"identity" text NOT NULL,
6+
"is_admin" boolean DEFAULT false,
7+
"joined_at" timestamp DEFAULT now() NOT NULL,
8+
"left_at" timestamp
9+
);
10+
--> statement-breakpoint
11+
CREATE TABLE "rooms" (
12+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
13+
"name" text NOT NULL,
14+
"sid" text NOT NULL,
15+
"created_by" uuid NOT NULL,
16+
"max_participants" integer DEFAULT 20,
17+
"created_at" timestamp DEFAULT now() NOT NULL,
18+
"updated_at" timestamp DEFAULT now() NOT NULL,
19+
"deleted_at" timestamp,
20+
CONSTRAINT "rooms_sid_unique" UNIQUE("sid")
21+
);
22+
--> statement-breakpoint
23+
CREATE TABLE "users" (
24+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
25+
"name" text NOT NULL,
26+
"email" text NOT NULL,
27+
"password" text NOT NULL,
28+
"created_at" timestamp DEFAULT now() NOT NULL,
29+
"updated_at" timestamp DEFAULT now() NOT NULL,
30+
"deleted_at" timestamp,
31+
CONSTRAINT "users_email_unique" UNIQUE("email")
32+
);
33+
--> statement-breakpoint
34+
ALTER TABLE "participants" ADD CONSTRAINT "participants_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
35+
ALTER TABLE "participants" ADD CONSTRAINT "participants_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
36+
ALTER TABLE "rooms" ADD CONSTRAINT "rooms_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;

apps/server/drizzle/0000_right_night_thrasher.sql

Lines changed: 0 additions & 10 deletions
This file was deleted.

apps/server/drizzle/meta/0000_snapshot.json

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,191 @@
11
{
2-
"id": "9ddaada5-a097-4fc3-a118-1d764288a820",
2+
"id": "088d4291-46ce-417a-9cac-c473a85afa9f",
33
"prevId": "00000000-0000-0000-0000-000000000000",
44
"version": "7",
55
"dialect": "postgresql",
66
"tables": {
7+
"public.participants": {
8+
"name": "participants",
9+
"schema": "",
10+
"columns": {
11+
"id": {
12+
"name": "id",
13+
"type": "uuid",
14+
"primaryKey": true,
15+
"notNull": true,
16+
"default": "gen_random_uuid()"
17+
},
18+
"room_id": {
19+
"name": "room_id",
20+
"type": "uuid",
21+
"primaryKey": false,
22+
"notNull": true
23+
},
24+
"user_id": {
25+
"name": "user_id",
26+
"type": "uuid",
27+
"primaryKey": false,
28+
"notNull": true
29+
},
30+
"identity": {
31+
"name": "identity",
32+
"type": "text",
33+
"primaryKey": false,
34+
"notNull": true
35+
},
36+
"is_admin": {
37+
"name": "is_admin",
38+
"type": "boolean",
39+
"primaryKey": false,
40+
"notNull": false,
41+
"default": false
42+
},
43+
"joined_at": {
44+
"name": "joined_at",
45+
"type": "timestamp",
46+
"primaryKey": false,
47+
"notNull": true,
48+
"default": "now()"
49+
},
50+
"left_at": {
51+
"name": "left_at",
52+
"type": "timestamp",
53+
"primaryKey": false,
54+
"notNull": false
55+
}
56+
},
57+
"indexes": {},
58+
"foreignKeys": {
59+
"participants_room_id_rooms_id_fk": {
60+
"name": "participants_room_id_rooms_id_fk",
61+
"tableFrom": "participants",
62+
"tableTo": "rooms",
63+
"columnsFrom": [
64+
"room_id"
65+
],
66+
"columnsTo": [
67+
"id"
68+
],
69+
"onDelete": "no action",
70+
"onUpdate": "no action"
71+
},
72+
"participants_user_id_users_id_fk": {
73+
"name": "participants_user_id_users_id_fk",
74+
"tableFrom": "participants",
75+
"tableTo": "users",
76+
"columnsFrom": [
77+
"user_id"
78+
],
79+
"columnsTo": [
80+
"id"
81+
],
82+
"onDelete": "no action",
83+
"onUpdate": "no action"
84+
}
85+
},
86+
"compositePrimaryKeys": {},
87+
"uniqueConstraints": {},
88+
"policies": {},
89+
"checkConstraints": {},
90+
"isRLSEnabled": false
91+
},
92+
"public.rooms": {
93+
"name": "rooms",
94+
"schema": "",
95+
"columns": {
96+
"id": {
97+
"name": "id",
98+
"type": "uuid",
99+
"primaryKey": true,
100+
"notNull": true,
101+
"default": "gen_random_uuid()"
102+
},
103+
"name": {
104+
"name": "name",
105+
"type": "text",
106+
"primaryKey": false,
107+
"notNull": true
108+
},
109+
"sid": {
110+
"name": "sid",
111+
"type": "text",
112+
"primaryKey": false,
113+
"notNull": true
114+
},
115+
"created_by": {
116+
"name": "created_by",
117+
"type": "uuid",
118+
"primaryKey": false,
119+
"notNull": true
120+
},
121+
"max_participants": {
122+
"name": "max_participants",
123+
"type": "integer",
124+
"primaryKey": false,
125+
"notNull": false,
126+
"default": 20
127+
},
128+
"created_at": {
129+
"name": "created_at",
130+
"type": "timestamp",
131+
"primaryKey": false,
132+
"notNull": true,
133+
"default": "now()"
134+
},
135+
"updated_at": {
136+
"name": "updated_at",
137+
"type": "timestamp",
138+
"primaryKey": false,
139+
"notNull": true,
140+
"default": "now()"
141+
},
142+
"deleted_at": {
143+
"name": "deleted_at",
144+
"type": "timestamp",
145+
"primaryKey": false,
146+
"notNull": false
147+
}
148+
},
149+
"indexes": {},
150+
"foreignKeys": {
151+
"rooms_created_by_users_id_fk": {
152+
"name": "rooms_created_by_users_id_fk",
153+
"tableFrom": "rooms",
154+
"tableTo": "users",
155+
"columnsFrom": [
156+
"created_by"
157+
],
158+
"columnsTo": [
159+
"id"
160+
],
161+
"onDelete": "no action",
162+
"onUpdate": "no action"
163+
}
164+
},
165+
"compositePrimaryKeys": {},
166+
"uniqueConstraints": {
167+
"rooms_sid_unique": {
168+
"name": "rooms_sid_unique",
169+
"nullsNotDistinct": false,
170+
"columns": [
171+
"sid"
172+
]
173+
}
174+
},
175+
"policies": {},
176+
"checkConstraints": {},
177+
"isRLSEnabled": false
178+
},
7179
"public.users": {
8180
"name": "users",
9181
"schema": "",
10182
"columns": {
11183
"id": {
12184
"name": "id",
13-
"type": "serial",
185+
"type": "uuid",
14186
"primaryKey": true,
15-
"notNull": true
187+
"notNull": true,
188+
"default": "gen_random_uuid()"
16189
},
17190
"name": {
18191
"name": "name",

apps/server/drizzle/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
{
66
"idx": 0,
77
"version": "7",
8-
"when": 1760873682221,
9-
"tag": "0000_right_night_thrasher",
8+
"when": 1761651395478,
9+
"tag": "0000_absurd_catseye",
1010
"breakpoints": true
1111
}
1212
]

apps/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"drizzle-orm": "^0.44.6",
2020
"hono": "^4.9.4",
2121
"jsonwebtoken": "^9.0.2",
22+
"livekit-server-sdk": "^2.14.0",
2223
"pino": "^10.1.0",
2324
"postgres": "^3.4.7",
2425
"zod": "^4.1.12"

apps/server/src/api/constants/errors.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,61 @@ export const API_ERRORS: Record<string, APIError> = {
1010
message: 'Bad Request',
1111
prettyMessage: 'The request was malformed or invalid.',
1212
},
13+
UNAUTHORIZED: {
14+
code: 401,
15+
message: 'Unauthorized',
16+
prettyMessage: 'You must be authenticated to access this resource.',
17+
},
1318
INVALID_CREDENTIALS: {
1419
code: 401,
1520
message: 'Invalid Credentials',
1621
prettyMessage: 'The provided email or password is incorrect.',
1722
},
23+
FORBIDDEN: {
24+
code: 403,
25+
message: 'Forbidden',
26+
prettyMessage: 'You do not have permission to access this resource.',
27+
},
1828
USER_NOT_FOUND: {
1929
code: 404,
2030
message: 'User Not Found',
2131
prettyMessage: 'The requested user could not be found.',
2232
},
33+
ROOM_NOT_FOUND: {
34+
code: 404,
35+
message: 'Room Not Found',
36+
prettyMessage: 'The requested room could not be found.',
37+
},
38+
PARTICIPANT_NOT_FOUND: {
39+
code: 404,
40+
message: 'Participant Not Found',
41+
prettyMessage: 'The requested participant could not be found.',
42+
},
2343
USER_ALREADY_EXISTS: {
2444
code: 409,
2545
message: 'User Already Exists',
2646
prettyMessage: 'A user with the provided email already exists.',
2747
},
48+
ALREADY_IN_ROOM: {
49+
code: 409,
50+
message: 'Already In Room',
51+
prettyMessage: 'You are already a participant in this room.',
52+
},
53+
ROOM_FULL: {
54+
code: 409,
55+
message: 'Room Full',
56+
prettyMessage: 'The room has reached its maximum participant capacity.',
57+
},
58+
ROOM_INACTIVE: {
59+
code: 410,
60+
message: 'Room Inactive',
61+
prettyMessage: 'The room is no longer active.',
62+
},
63+
NOT_ROOM_ADMIN: {
64+
code: 403,
65+
message: 'Not Room Admin',
66+
prettyMessage: 'Only the room admin can perform this action.',
67+
},
2868
INTERNAL_SERVER_ERROR: {
2969
code: 500,
3070
message: 'Internal Server Error',
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Context } from 'hono'
2+
import { API_ERRORS } from '../constants/errors.ts'
3+
import { getParticipantValidator } from '../validators/participant.validators.ts'
4+
import { participantRepository } from '../../database/repositories/participants.repository.ts'
5+
6+
export const getParticipant = async (c: Context) => {
7+
const logger = c.get('logger')
8+
const db = c.get('db')
9+
10+
try {
11+
const participantId = c.req.param('participantId')
12+
const validate = getParticipantValidator({ participantId })
13+
14+
if (!validate.success) {
15+
logger.warn({ error: validate.error }, 'Invalid get participant request')
16+
return c.json({ error: API_ERRORS.BAD_REQUEST }, 400)
17+
}
18+
19+
const participant = await participantRepository(db).getById(participantId)
20+
if (!participant) {
21+
return c.json({ error: API_ERRORS.PARTICIPANT_NOT_FOUND }, 404)
22+
}
23+
24+
return c.json({ data: { participant } }, 200)
25+
} catch (error) {
26+
logger.error({ error }, 'Failed to get participant')
27+
return c.json({ error: API_ERRORS.INTERNAL_SERVER_ERROR }, 500)
28+
}
29+
}
30+
31+
export const getUserParticipations = async (c: Context) => {
32+
const logger = c.get('logger')
33+
const db = c.get('db')
34+
35+
try {
36+
const userId = c.get('userId')
37+
if (!userId) {
38+
return c.json({ error: API_ERRORS.UNAUTHORIZED }, 401)
39+
}
40+
41+
const participations = await participantRepository(db).getByUserId(userId)
42+
43+
return c.json({ data: { participations } }, 200)
44+
} catch (error) {
45+
logger.error({ error }, 'Failed to get user participations')
46+
return c.json({ error: API_ERRORS.INTERNAL_SERVER_ERROR }, 500)
47+
}
48+
}

0 commit comments

Comments
 (0)