-
Notifications
You must be signed in to change notification settings - Fork 0
Dev #15
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 #15
Changes from all commits
6ec90f3
e832394
ec6fa79
ca3de8e
1f3d2c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,18 +17,76 @@ | |||||||||||||||||||
| - [x] Refactor DM membership checks into a shared helper and remove redundant member remapping in `getDM`. | ||||||||||||||||||||
| - [x] Move realtime runtime config to validated env values (`REALTIME_PORT`, `REALTIME_CORS_ORIGIN`). | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Next (Short-Term Hardening) | ||||||||||||||||||||
| --- | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 1 — Core UX Gaps | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [x] File/image attachment uploads (R2) | ||||||||||||||||||||
| - [ ] Message deletion | ||||||||||||||||||||
|
Comment on lines
+22
to
+25
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. Add reply support to the shipped roadmap items. This PR ships message replies as well as attachments, but the roadmap update only reflects the attachment half. That leaves 📝 Suggested update-## Phase 1 — Core UX Gaps
-
-- [x] File/image attachment uploads (R2)
+## Phase 1 — Core UX Gaps
+
+- [x] Message replies
+- [x] File/image attachment uploads📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| - [ ] Message editing UI | ||||||||||||||||||||
| - [ ] User profiles (bio, custom status, avatar upload) | ||||||||||||||||||||
| - [ ] Channel edit/delete | ||||||||||||||||||||
| - [ ] Settings pages | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 2 — Permissions & Moderation | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Granular permission system (beyond owner/admin/member) | ||||||||||||||||||||
| - [ ] Member management UI (kick, banish, silence, role assignment) | ||||||||||||||||||||
| - [ ] Rate limiting enforcement (API-level + per-channel) | ||||||||||||||||||||
| - [ ] Audit logs | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 3 — Social Features | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Shareable invite links (not just email invites) | ||||||||||||||||||||
| - [ ] Ally (friend) system with requests | ||||||||||||||||||||
| - [ ] User blocking | ||||||||||||||||||||
| - [ ] Privacy settings | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 4 — Tests & CI/CD | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] API endpoint tests | ||||||||||||||||||||
| - [ ] Critical path integration tests | ||||||||||||||||||||
| - [ ] CI pipeline (lint, type-check, test, build) | ||||||||||||||||||||
| - [ ] Docker / deployment configs | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 5 — Polish | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Message search | ||||||||||||||||||||
| - [ ] Typing indicators | ||||||||||||||||||||
| - [ ] Pinned messages panel | ||||||||||||||||||||
| - [ ] Thread support | ||||||||||||||||||||
| - [ ] Notification preferences | ||||||||||||||||||||
| - [ ] Error handling & loading state improvements | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 6 — Infrastructure | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Structured logger (Pino/Winston) replacing `console.error` | ||||||||||||||||||||
| - [ ] Production environment management | ||||||||||||||||||||
| - [ ] Production startup guard for `REALTIME_CORS_ORIGIN` on localhost defaults | ||||||||||||||||||||
| - [ ] Database migration workflow | ||||||||||||||||||||
| - [ ] Monitoring & logging (observability) | ||||||||||||||||||||
| - [ ] CORS lockdown for production domains | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Phase 7 — v2 Features | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Voice/video (Voice Chambers) | ||||||||||||||||||||
| - [ ] Bots & webhooks | ||||||||||||||||||||
| - [ ] Custom emojis (Sigils & Crests) | ||||||||||||||||||||
| - [ ] Server discovery | ||||||||||||||||||||
| - [ ] Forum channel posts | ||||||||||||||||||||
|
|
||||||||||||||||||||
| --- | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Backlog (Short-Term Hardening) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Add explicit error logging in `initializeConnection` before disconnecting a socket (include `socket.id` + `userId` context). | ||||||||||||||||||||
| - [ ] Replace `console.error` with a proper structured logger package (e.g. Pino or Winston) and wire logs for external observability platforms (Datadog/New Relic). | ||||||||||||||||||||
| - [ ] Update onboarding `normalizeSlugInput` to collapse repeated hyphens while typing. | ||||||||||||||||||||
| - [ ] Use `DM_CHANNEL_TYPES` constant everywhere in DM route filters to avoid drift. | ||||||||||||||||||||
| - [ ] Extract shared DM last-message projection/formatter helper so list/get endpoints use one source of truth. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Later (Architecture / Scale) | ||||||||||||||||||||
| ## Backlog (Architecture / Scale) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [ ] Move `@everyone` fanout off the realtime request path: | ||||||||||||||||||||
| - [ ] Enqueue one guild-fanout job instead of per-member synchronous work in `message:send`. | ||||||||||||||||||||
| - [ ] Process fanout in a worker with batched DB writes and chunked emits. | ||||||||||||||||||||
| - [ ] Add rate-limiting and permission checks for mass mentions. | ||||||||||||||||||||
| - [ ] Add a production startup guard that fails fast (or loudly warns) when `REALTIME_CORS_ORIGIN` is left on localhost defaults. | ||||||||||||||||||||
| - [ ] Extract shared DM last-message projection/formatter helper so list/get endpoints use one source of truth. | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { S3Client } from "@aws-sdk/client-s3" | ||
| import { env } from "@repo/env/server" | ||
|
|
||
| export const s3Client = new S3Client({ | ||
| region: env.S3_REGION, | ||
| endpoint: env.S3_ENDPOINT, | ||
| credentials: { | ||
| accessKeyId: env.S3_ACCESS_KEY_ID, | ||
| secretAccessKey: env.S3_SECRET_ACCESS_KEY, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { PutObjectCommand } from "@aws-sdk/client-s3" | ||
| import { getSignedUrl } from "@aws-sdk/s3-request-presigner" | ||
| import { db } from "@repo/db" | ||
| import { channel, channelMember, guildMember } from "@repo/db/schema" | ||
| import { env } from "@repo/env/server" | ||
| import { and, eq } from "drizzle-orm" | ||
| import * as HttpStatusCodes from "@/lib/helpers/http/status-codes" | ||
| import { s3Client } from "@/lib/s3" | ||
| import type { AppRouteHandler } from "@/lib/types/app-types" | ||
| import type { PresignRoute } from "./routes" | ||
| import { PRESIGNED_URL_EXPIRY_SECONDS } from "./schema" | ||
|
|
||
| const DM_CHANNEL_TYPES = ["dm", "group_dm"] as const | ||
|
|
||
| export const presign: AppRouteHandler<PresignRoute> = async (c) => { | ||
| const user = c.var.user | ||
| const { channelId, filename, contentType, size } = c.req.valid("json") | ||
|
|
||
| if (size > env.MAX_FILE_UPLOAD_SIZE) { | ||
| return c.json( | ||
| { success: false, message: "File too large" }, | ||
| HttpStatusCodes.REQUEST_TOO_LONG | ||
| ) | ||
| } | ||
|
Comment on lines
+19
to
+24
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. Consider using HTTP 413 for payload size violations. Returning Suggested fix if (size > env.MAX_FILE_UPLOAD_SIZE) {
return c.json(
{ success: false, message: "File too large" },
- HttpStatusCodes.FORBIDDEN
+ HttpStatusCodes.CONTENT_TOO_LARGE
)
}🤖 Prompt for AI Agents |
||
|
|
||
| // Fetch the channel to determine access check strategy | ||
| const ch = await db | ||
| .select({ id: channel.id, guildId: channel.guildId, type: channel.type }) | ||
| .from(channel) | ||
| .where(eq(channel.id, channelId)) | ||
| .limit(1) | ||
| .then((rows) => rows[0]) | ||
|
|
||
| if (!ch) { | ||
| return c.json( | ||
| { success: false, message: "Forbidden" }, | ||
| HttpStatusCodes.FORBIDDEN | ||
| ) | ||
| } | ||
|
|
||
| // Guild channel — verify guild membership | ||
| if (ch.guildId) { | ||
| const member = await db | ||
| .select({ id: guildMember.id }) | ||
| .from(guildMember) | ||
| .where( | ||
| and( | ||
| eq(guildMember.guildId, ch.guildId), | ||
| eq(guildMember.userId, user.id) | ||
| ) | ||
| ) | ||
| .limit(1) | ||
| .then((rows) => rows[0]) | ||
|
|
||
| if (!member) { | ||
| return c.json( | ||
| { success: false, message: "Forbidden" }, | ||
| HttpStatusCodes.FORBIDDEN | ||
| ) | ||
| } | ||
| } else if ( | ||
| DM_CHANNEL_TYPES.includes(ch.type as (typeof DM_CHANNEL_TYPES)[number]) | ||
| ) { | ||
| // DM/group DM — verify channel membership | ||
| const member = await db | ||
| .select({ id: channelMember.id }) | ||
| .from(channelMember) | ||
| .where( | ||
| and( | ||
| eq(channelMember.channelId, channelId), | ||
| eq(channelMember.userId, user.id) | ||
| ) | ||
| ) | ||
| .limit(1) | ||
| .then((rows) => rows[0]) | ||
|
|
||
| if (!member) { | ||
| return c.json( | ||
| { success: false, message: "Forbidden" }, | ||
| HttpStatusCodes.FORBIDDEN | ||
| ) | ||
| } | ||
| } else { | ||
| // Unknown channel type with no guild — reject | ||
| return c.json( | ||
| { success: false, message: "Forbidden" }, | ||
| HttpStatusCodes.FORBIDDEN | ||
| ) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const fileId = crypto.randomUUID() | ||
| const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, "_") | ||
| const key = `attachments/${channelId}/${fileId}/${sanitizedFilename}` | ||
|
|
||
| const command = new PutObjectCommand({ | ||
| Bucket: env.S3_BUCKET_NAME, | ||
| Key: key, | ||
| ContentType: contentType, | ||
| ContentLength: size, | ||
| }) | ||
|
|
||
| const uploadUrl = await getSignedUrl(s3Client, command, { | ||
| expiresIn: PRESIGNED_URL_EXPIRY_SECONDS, | ||
| }) | ||
|
|
||
| const fileUrl = `${env.S3_PUBLIC_URL.replace(/\/$/, "")}/${key}` | ||
|
|
||
| return c.json({ uploadUrl, fileUrl, key }, HttpStatusCodes.OK) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { createRouter } from "@/lib/helpers/app/create-app" | ||
| import * as handlers from "./handlers" | ||
| import * as routes from "./routes" | ||
|
|
||
| const uploadsRouter = createRouter().openapi(routes.presign, handlers.presign) | ||
|
|
||
| export default uploadsRouter |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| 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, | ||
| payloadTooLargeSchema, | ||
| unauthorizedSchema, | ||
| } from "@/lib/helpers/openapi/schemas" | ||
| import { sessionAuthMiddleware } from "@/middleware/session-auth" | ||
| import { presignRequestSchema, presignResponseSchema } from "./schema" | ||
|
|
||
| export const presign = createRoute({ | ||
| path: "/uploads/presign", | ||
| method: "post", | ||
| summary: "Request a presigned upload URL", | ||
| description: | ||
| "Returns a presigned URL for direct upload to S3-compatible storage.", | ||
| tags: ["Uploads"], | ||
| middleware: [sessionAuthMiddleware] as const, | ||
| request: { | ||
| body: jsonContent({ | ||
| schema: presignRequestSchema, | ||
| description: "File metadata for upload", | ||
| }), | ||
| }, | ||
| responses: { | ||
| [HttpStatusCodes.OK]: jsonContent({ | ||
| schema: presignResponseSchema, | ||
| description: "Presigned URL for upload", | ||
| }), | ||
| [HttpStatusCodes.UNAUTHORIZED]: unauthorizedSchema, | ||
| [HttpStatusCodes.FORBIDDEN]: forbiddenSchema, | ||
| [HttpStatusCodes.REQUEST_TOO_LONG]: payloadTooLargeSchema, | ||
| [HttpStatusCodes.INTERNAL_SERVER_ERROR]: internalServerErrorSchema, | ||
| }, | ||
| }) | ||
|
|
||
| export type PresignRoute = typeof presign |
Uh oh!
There was an error while loading. Please reload this page.