-
Notifications
You must be signed in to change notification settings - Fork 0
Dev #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev #3
Changes from all commits
b083272
b62e9ae
74ca351
7ed06b8
d4890f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { auth } from "@repo/auth" | ||
| import { db } from "@repo/db" | ||
| import { guild, guildMember } from "@repo/db/schema" | ||
| import { and, eq } from "drizzle-orm" | ||
| import type { Context, Next } from "hono" | ||
| import * as HttpStatusCodes from "@/lib/helpers/http/status-codes" | ||
| import type { AppBindings } from "@/lib/types/app-types" | ||
|
|
||
| /** | ||
| * Authenticates the request via better-auth session and resolves the | ||
| * user's active guild + membership. | ||
| * | ||
| * Sets in context: | ||
| * - user: The authenticated user | ||
| * - session: The session object | ||
| * - guild: The user's active guild | ||
| * - member: The user's membership in the active guild | ||
| */ | ||
| export const authMiddleware = async (c: Context<AppBindings>, next: Next) => { | ||
| const session = await auth.api.getSession({ headers: c.req.raw.headers }) | ||
|
|
||
| if (!session) { | ||
| return c.json( | ||
| { success: false, message: "Unauthorized" }, | ||
| HttpStatusCodes.UNAUTHORIZED | ||
| ) | ||
| } | ||
|
|
||
| const activeGuildId = session.session.activeOrganizationId | ||
|
|
||
| if (!activeGuildId) { | ||
| return c.json( | ||
| { success: false, message: "No active guild selected" }, | ||
| HttpStatusCodes.BAD_REQUEST | ||
| ) | ||
| } | ||
|
|
||
| const memberRecord = await db | ||
| .select() | ||
| .from(guildMember) | ||
| .where( | ||
| and( | ||
| eq(guildMember.userId, session.user.id), | ||
| eq(guildMember.guildId, activeGuildId) | ||
| ) | ||
| ) | ||
| .limit(1) | ||
| .then((rows) => rows[0]) | ||
|
|
||
| if (!memberRecord) { | ||
| return c.json( | ||
| { success: false, message: "You are not a member of this guild" }, | ||
| HttpStatusCodes.FORBIDDEN | ||
| ) | ||
| } | ||
|
|
||
| const guildRecord = await db | ||
| .select() | ||
| .from(guild) | ||
| .where(eq(guild.id, activeGuildId)) | ||
| .limit(1) | ||
| .then((rows) => rows[0]) | ||
|
|
||
| if (!guildRecord) { | ||
| return c.json( | ||
| { success: false, message: "Guild not found" }, | ||
| HttpStatusCodes.NOT_FOUND | ||
| ) | ||
| } | ||
|
|
||
| c.set("user", session.user) | ||
| c.set("session", session.session) | ||
| c.set("guild", guildRecord) | ||
| c.set("member", memberRecord) | ||
|
|
||
| await next() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { db } from "@repo/db" | ||
| import { channel } from "@repo/db/schema" | ||
| import { eq } from "drizzle-orm" | ||
| import * as HttpStatusCodes from "@/lib/helpers/http/status-codes" | ||
| import type { AppRouteHandler } from "@/lib/types/app-types" | ||
| import type { CreateChannelRoute, ListChannelsRoute } from "./routes" | ||
|
|
||
| export const listChannels: AppRouteHandler<ListChannelsRoute> = async (c) => { | ||
| const guild = c.var.guild | ||
|
|
||
| const channels = await db | ||
| .select() | ||
| .from(channel) | ||
| .where(eq(channel.guildId, guild.id)) | ||
|
|
||
| return c.json({ success: true, data: channels }, HttpStatusCodes.OK) | ||
| } | ||
|
|
||
| export const createChannel: AppRouteHandler<CreateChannelRoute> = async (c) => { | ||
| const guild = c.var.guild | ||
| const body = c.req.valid("json") | ||
|
|
||
| const newChannel = await db | ||
| .insert(channel) | ||
| .values({ | ||
| ...body, | ||
| guildId: guild.id, | ||
| }) | ||
| .returning() | ||
| .then((rows) => rows[0]) | ||
|
|
||
| if (!newChannel) { | ||
| return c.json( | ||
| { success: false, message: "Internal server error" }, | ||
| HttpStatusCodes.INTERNAL_SERVER_ERROR | ||
| ) | ||
| } | ||
|
|
||
| return c.json({ success: true, data: newChannel }, HttpStatusCodes.CREATED) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { createRouter } from "@/lib/helpers/app/create-app" | ||
| import * as handlers from "./handlers" | ||
| import * as routes from "./routes" | ||
|
|
||
| const channelsRouter = createRouter() | ||
| .openapi(routes.listChannels, handlers.listChannels) | ||
| .openapi(routes.createChannel, handlers.createChannel) | ||
|
|
||
| export default channelsRouter |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { createRoute } from "@hono/zod-openapi" | ||
| import * as HttpStatusCodes from "@/lib/helpers/http/status-codes" | ||
| import jsonContent from "@/lib/helpers/openapi/json-content" | ||
| import { | ||
| forbiddenSchema, | ||
| internalServerErrorSchema, | ||
| unauthorizedSchema, | ||
| } from "@/lib/helpers/openapi/schemas" | ||
| import { authMiddleware } from "@/middleware/auth" | ||
| import { | ||
| createChannelRequestSchema, | ||
| createChannelResponseSchema, | ||
| listChannelsResponseSchema, | ||
| } from "./schema" | ||
|
|
||
| export const listChannels = createRoute({ | ||
| path: "/channels", | ||
| method: "get", | ||
| summary: "List channels", | ||
| description: "Lists all channels in the user's active guild.", | ||
| tags: ["Channels"], | ||
| middleware: [authMiddleware] as const, | ||
| responses: { | ||
| [HttpStatusCodes.OK]: jsonContent({ | ||
| schema: listChannelsResponseSchema, | ||
| description: "List of channels", | ||
| }), | ||
| [HttpStatusCodes.UNAUTHORIZED]: unauthorizedSchema, | ||
| [HttpStatusCodes.FORBIDDEN]: forbiddenSchema, | ||
| [HttpStatusCodes.INTERNAL_SERVER_ERROR]: internalServerErrorSchema, | ||
| }, | ||
| }) | ||
|
|
||
| export const createChannel = createRoute({ | ||
| path: "/channels", | ||
| method: "post", | ||
| summary: "Create a channel", | ||
| description: | ||
| "Creates a new channel in the user's active guild. Guild ID is derived from the session.", | ||
| tags: ["Channels"], | ||
| middleware: [authMiddleware] as const, | ||
| request: { | ||
| body: jsonContent({ | ||
| schema: createChannelRequestSchema, | ||
| description: "Channel details", | ||
| }), | ||
| }, | ||
| responses: { | ||
| [HttpStatusCodes.CREATED]: jsonContent({ | ||
| schema: createChannelResponseSchema, | ||
| description: "Channel created", | ||
| }), | ||
| [HttpStatusCodes.UNAUTHORIZED]: unauthorizedSchema, | ||
| [HttpStatusCodes.FORBIDDEN]: forbiddenSchema, | ||
| [HttpStatusCodes.INTERNAL_SERVER_ERROR]: internalServerErrorSchema, | ||
| }, | ||
| }) | ||
|
Comment on lines
+16
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OpenAPI responses are missing Both routes can return 400 (middleware: "No active guild selected"; POST also from zod validation failures) and 404 (middleware: "Guild not found"). Documenting these in the OpenAPI spec ensures accurate client codegen and API docs. 🤖 Prompt for AI Agents |
||
|
|
||
| export type ListChannelsRoute = typeof listChannels | ||
| export type CreateChannelRoute = typeof createChannel | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { z } from "@hono/zod-openapi" | ||
| import { insertChannelSchema, selectChannelSchema } from "@repo/db/schema" | ||
|
|
||
| export const channelResponseSchema = selectChannelSchema | ||
|
|
||
| export const listChannelsResponseSchema = z.object({ | ||
| success: z.literal(true), | ||
| data: z.array(selectChannelSchema), | ||
| }) | ||
|
|
||
| export const createChannelRequestSchema = insertChannelSchema | ||
|
|
||
| export const createChannelResponseSchema = z.object({ | ||
| success: z.literal(true), | ||
| data: selectChannelSchema, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { cn } from "@repo/ui/lib/utils" | ||
| import { Hash, Volume2 } from "lucide-react" | ||
| import { UserAvatar } from "../user-avatar" | ||
|
|
||
| // Hardcoded mock data — will be replaced with real data | ||
| const channels = [ | ||
| { name: "general", active: true, unread: false }, | ||
| { name: "introductions", active: false, unread: true }, | ||
| { name: "development", active: false, unread: false }, | ||
| { name: "design", active: false, unread: false }, | ||
| { name: "off-topic", active: false, unread: true }, | ||
| ] | ||
|
|
||
| const voiceChannels = [ | ||
| { name: "Lounge", usersIn: ["Sam Chen", "Jordan Blake"] }, | ||
| { name: "Dev Session", usersIn: [] as string[] }, | ||
| ] | ||
|
|
||
| export function ChannelList() { | ||
| return ( | ||
| <nav> | ||
| {/* Text Channels */} | ||
| <span className="mb-1 block px-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"> | ||
| Channels | ||
| </span> | ||
| {channels.map((ch) => ( | ||
| <div | ||
| key={ch.name} | ||
| className={cn( | ||
| "relative flex items-center gap-2 rounded-lg px-2 py-[6px] text-[14px]", | ||
| ch.active | ||
| ? "bg-foreground/[0.06] font-medium text-foreground" | ||
| : ch.unread | ||
| ? "font-medium text-foreground" | ||
| : "text-muted-foreground" | ||
| )} | ||
| > | ||
| {ch.active && ( | ||
| <div className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-r-full bg-primary" /> | ||
| )} | ||
| <Hash className="size-[16px] shrink-0 opacity-50" /> | ||
| <span className="truncate">{ch.name}</span> | ||
| {ch.unread && !ch.active && ( | ||
| <div className="ml-auto size-1.5 shrink-0 rounded-full bg-primary" /> | ||
| )} | ||
| </div> | ||
| ))} | ||
|
|
||
| {/* Voice Channels */} | ||
| <span className="mb-1 mt-5 block px-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"> | ||
| Voice | ||
| </span> | ||
| {voiceChannels.map((ch) => ( | ||
| <div key={ch.name}> | ||
| <div className="flex items-center gap-2 rounded-lg px-2 py-[6px] text-[14px] text-muted-foreground"> | ||
| <Volume2 className="size-[16px] shrink-0 opacity-50" /> | ||
| <span className="truncate">{ch.name}</span> | ||
| {ch.usersIn.length > 0 && ( | ||
| <div className="ml-auto flex items-center gap-1"> | ||
| <div className="flex gap-[3px]"> | ||
| <div className="h-3 w-[2px] animate-pulse rounded-full bg-emerald-500" /> | ||
| <div className="h-2 w-[2px] animate-pulse rounded-full bg-emerald-500 [animation-delay:150ms]" /> | ||
| <div className="h-3.5 w-[2px] animate-pulse rounded-full bg-emerald-500 [animation-delay:300ms]" /> | ||
| </div> | ||
| <span className="text-[11px] text-emerald-600"> | ||
| {ch.usersIn.length} | ||
| </span> | ||
| </div> | ||
| )} | ||
| </div> | ||
| {ch.usersIn.length > 0 && ( | ||
| <div className="mb-1 ml-2 space-y-px"> | ||
| {ch.usersIn.map((name) => ( | ||
| <div | ||
| key={name} | ||
| className="flex items-center gap-2 rounded-md px-2 py-1 text-[13px] text-muted-foreground" | ||
| > | ||
| <UserAvatar name={name} size="sm" /> | ||
| <span className="truncate">{name}</span> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ))} | ||
| </nav> | ||
| ) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.