Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ async function bootstrap() {

app.setGlobalPrefix('api');

const allowedOrigins = [
process.env.FRONTEND_URL,
'http://localhost:3001',
'http://127.0.0.1:3001',
'http://localhost:8080',
'http://127.0.0.1:8080',
].filter(Boolean) as string[];
Comment on lines +14 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Dev-only origins remain allowed in production.

allowedOrigins unconditionally includes localhost:3001/127.0.0.1:3001/:8080 dev URLs alongside FRONTEND_URL, with no environment gating. In production this means the credentialed CORS allowlist always trusts local dev origins, unnecessarily widening the trust boundary for a credentials-enabled API.

🔒 Proposed fix — gate dev origins by environment
-  const allowedOrigins = [
-    process.env.FRONTEND_URL,
-    'http://localhost:3001',
-    'http://127.0.0.1:3001',
-    'http://localhost:8080',
-    'http://127.0.0.1:8080',
-  ].filter(Boolean) as string[];
+  const devOrigins = [
+    'http://localhost:3001',
+    'http://127.0.0.1:3001',
+    'http://localhost:8080',
+    'http://127.0.0.1:8080',
+  ];
+  const allowedOrigins = [
+    process.env.FRONTEND_URL,
+    ...(process.env.NODE_ENV !== 'production' ? devOrigins : []),
+  ].filter(Boolean) as string[];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const allowedOrigins = [
process.env.FRONTEND_URL,
'http://localhost:3001',
'http://127.0.0.1:3001',
'http://localhost:8080',
'http://127.0.0.1:8080',
].filter(Boolean) as string[];
const devOrigins = [
'http://localhost:3001',
'http://127.0.0.1:3001',
'http://localhost:8080',
'http://127.0.0.1:8080',
];
const allowedOrigins = [
process.env.FRONTEND_URL,
...(process.env.NODE_ENV !== 'production' ? devOrigins : []),
].filter(Boolean) as string[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/main.ts` around lines 14 - 20, The CORS allowlist in
allowedOrigins currently mixes FRONTEND_URL with localhost/127.0.0.1 dev URLs
unconditionally, which leaves dev origins trusted in production. Update
backend/src/main.ts so the allowedOrigins construction in the startup/CORS setup
only appends the localhost/127.0.0.1 entries when running in a development
environment, and keep production limited to the configured FRONTEND_URL; use the
existing allowedOrigins symbol so the gating is easy to find.


app.enableCors({
origin: process.env.NODE_ENV === 'production'
? process.env.FRONTEND_URL
: true,
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
return;
}
callback(null, false);
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id'],
});

app.useWebSocketAdapter(new WsAuthAdapter(app));
Expand Down
28 changes: 15 additions & 13 deletions backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"rootDir": "./src",
"outDir": "./dist",
"baseUrl": "./",
"ignoreDeprecations": "6.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check pinned typescript version in backend
fd package.json backend -x jq -r '.devDependencies.typescript // .dependencies.typescript' {}

Repository: HappyHackingSpace/CommunityHub

Length of output: 175


🌐 Web query:

TypeScript ignoreDeprecations "6.0" 5.7.3 diagnostic TS5103 docs

💡 Result:

In TypeScript 5.7.3, the compiler option "ignoreDeprecations": "6.0" is invalid and will trigger diagnostic error TS5103: "Invalid value for '--ignoreDeprecations'" [1][2][3]. The "ignoreDeprecations": "6.0" setting is a feature introduced specifically in TypeScript 6.0 to allow developers to suppress deprecation warnings for features that are planned for removal in TypeScript 7.0 [4]. Because TypeScript 5.7.3 predates the 6.0 release, it does not recognize "6.0" as a valid argument for this configuration option [4][5]. If you are encountering deprecation warnings in TypeScript 5.7.3, you must use a version-appropriate value (such as "5.0") if supported by your specific environment, or address the underlying configuration issue directly by removing or updating the deprecated feature [6][1]. Attempting to use "6.0" in any TypeScript 5.x version will consistently result in error TS5103 [1][2][3].

Citations:


backend/tsconfig.json uses a TypeScript 6-only value
backend/package.json pins TypeScript ^5.7.3, so ignoreDeprecations: "6.0" will fail on this branch with TS5103. Either switch to a 5.x-compatible value or upgrade TypeScript to 6.0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tsconfig.json` at line 18, The tsconfig setting uses a TypeScript
6-only deprecation target, which conflicts with the currently pinned TypeScript
5.7.x version. Update the backend/tsconfig.json setting for ignoreDeprecations
to a 5.x-compatible value, or alternatively align the TypeScript version in
backend/package.json to 6.0; check the tsconfig field and the package.json
TypeScript dependency together so they match.

"paths": {
"src/*": ["src/*"],
"@/*": ["src/*"]
Expand Down
5 changes: 4 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
AUTH_SECRET={AUTH_SECRET}
AUTH_SECRET=XKZ7Z1O3bhU4caz2D8ouu7dKhXyQ+ZUhsjvTwtHxp10=
NEXTAUTH_SECRET=XKZ7Z1O3bhU4caz2D8ouu7dKhXyQ+ZUhsjvTwtHxp10=
Comment on lines +1 to +2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use an obvious placeholder instead of a real-looking secret value.

Both AUTH_SECRET and NEXTAUTH_SECRET use an identical, well-formed base64 string. Secret scanners flag this as a potential real credential, and if ever copy-pasted verbatim into a real .env, it provides no security. Prefer a clearly fake placeholder (e.g., AUTH_SECRET=changeme-generate-with-openssl-rand-base64-32).

🧰 Tools
🪛 Betterleaks (1.6.0)

[high] 1-1: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 2-2: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/.env.example` around lines 1 - 2, The .env example secrets are using
a real-looking base64 value, which can be flagged by secret scanners and copied
into real environments accidentally. Update the placeholders for AUTH_SECRET and
NEXTAUTH_SECRET in the .env.example file to obviously fake, non-secret values
(for example, a “changeme” style placeholder) so it is clear they must be
replaced before use.

Source: Linters/SAST tools

AUTH_URL=http://localhost:3001
NEXTAUTH_URL=http://localhost:3001
AUTH_GOOGLE_ID={AUTH_GOOGLE_ID}
AUTH_GOOGLE_SECRET={AUTH_GOOGLE_SECRET}
87 changes: 87 additions & 0 deletions frontend/src/app/dashboard/dashboard-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import { CreatePostInput } from "@/components/dashboard/create-post-input";
import { PostCard } from "@/components/dashboard/post-card";
import { RightSidebar } from "@/components/dashboard/right-sidebar";

import { useActivityFeed } from "@/hooks/use-activity-feed";
import { useCommunities } from "@/hooks/use-communities";
import { useTenantContext } from "@/components/providers/tenant-provider";
import { useSession } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";

export function DashboardContent() {
const { data: session } = useSession();
const { communities, isLoading: communitiesLoading } = useCommunities();
const { tenantId: currentTenantId, setTenantId } = useTenantContext();
const router = useRouter();
const searchParams = useSearchParams();
const tenantIdFromUrl = searchParams.get("tenantId");

const { posts, isLoading, error, createPost, toggleLike, deletePost, editPost } = useActivityFeed();

useEffect(() => {
if (tenantIdFromUrl && tenantIdFromUrl !== currentTenantId) {
setTenantId(tenantIdFromUrl);
}
}, [tenantIdFromUrl, currentTenantId, setTenantId]);

useEffect(() => {
if (!communitiesLoading && communities.length > 0 && !tenantIdFromUrl) {
const myCommunity = communities.find(
(c) => c.founderId === session?.user?.id || c.isMember
);

if (myCommunity) {
router.replace(`/dashboard?tenantId=${myCommunity.tenantId || myCommunity.id}`);
}
}
}, [communities, communitiesLoading, tenantIdFromUrl, router, session]);

return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 pb-20">
<div className="lg:col-span-8 xl:col-span-9 flex flex-col">
<CreatePostInput onPost={createPost} />

<div className="flex flex-col gap-2">
{isLoading ? (
<div className="text-center py-8 text-muted-foreground flex items-center justify-center gap-2">
<span className="loading loading-spinner loading-sm"></span> Loading feed...
</div>
) : error ? (
<div className="text-center py-8 text-red-500">
Failed to load activity feed.
</div>
) : posts.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No posts yet. Be the first to write something!
</div>
) : (
<>
{posts.map((post) => (
<PostCard
key={post.id}
post={post}
onLike={() => toggleLike(post.id, post.isLikedByMe || false)}
onDelete={() => deletePost(post.id)}
onEdit={(newContent) => editPost(post.id, newContent)}
/>
))}

<div className="text-center text-muted-foreground text-sm py-8">
You've caught up with all the latest activity!
</div>
</>
)}
</div>
</div>

<div className="lg:col-span-4 xl:col-span-3">
<div className="sticky top-[88px]">
<RightSidebar />
</div>
</div>
</div>
);
}
94 changes: 5 additions & 89 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,10 @@
"use client";

import { CreatePostInput } from "@/components/dashboard/create-post-input";
import { PostCard } from "@/components/dashboard/post-card";
import { RightSidebar } from "@/components/dashboard/right-sidebar";

import { useActivityFeed } from "@/hooks/use-activity-feed";
import { useCommunities } from "@/hooks/use-communities";
import { useTenantContext } from "@/components/providers/tenant-provider";
import { useSession } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { Suspense } from "react";
import { DashboardContent } from "./dashboard-content";

export default function DashboardPage() {
const { data: session } = useSession();
const { communities, isLoading: communitiesLoading } = useCommunities();
const { tenantId: currentTenantId, setTenantId } = useTenantContext();
const router = useRouter();
const searchParams = useSearchParams();
const tenantIdFromUrl = searchParams.get("tenantId");

const { posts, isLoading, error, createPost, toggleLike, deletePost, editPost } = useActivityFeed();

// Sync URL tenantId to context
useEffect(() => {
if (tenantIdFromUrl && tenantIdFromUrl !== currentTenantId) {
setTenantId(tenantIdFromUrl);
}
}, [tenantIdFromUrl, currentTenantId, setTenantId]);

useEffect(() => {
if (!communitiesLoading && communities.length > 0 && !tenantIdFromUrl) {
const myCommunity = communities.find(
(c) => c.founderId === session?.user?.id || c.isMember
);

if (myCommunity) {
router.replace(`/dashboard?tenantId=${myCommunity.tenantId || myCommunity.id}`);
}
}
}, [communities, communitiesLoading, tenantIdFromUrl, router, session]);

return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 pb-20">

{/* Main Feed Column */}
<div className="lg:col-span-8 xl:col-span-9 flex flex-col">
<CreatePostInput onPost={createPost} />

{/* Posts List */}
<div className="flex flex-col gap-2">
{isLoading ? (
<div className="text-center py-8 text-muted-foreground flex items-center justify-center gap-2">
<span className="loading loading-spinner loading-sm"></span> Loading feed...
</div>
) : error ? (
<div className="text-center py-8 text-red-500">
Failed to load activity feed.
</div>
) : posts.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No posts yet. Be the first to write something!
</div>
) : (
<>
{posts.map((post) => (
<PostCard
key={post.id}
post={post}
onLike={() => toggleLike(post.id, post.isLikedByMe || false)}
onDelete={() => deletePost(post.id)}
onEdit={(newContent) => editPost(post.id, newContent)}
/>
))}

{/* End of feed message */}
<div className="text-center text-muted-foreground text-sm py-8">
You've caught up with all the latest activity!
</div>
</>
)}
</div>
</div>

{/* Right Sidebar Column */}
<div className="lg:col-span-4 xl:col-span-3">
<div className="sticky top-[88px]">
<RightSidebar />
</div>
</div>

</div>
<Suspense fallback={<div className="text-center py-8 text-muted-foreground">Loading dashboard...</div>}>
<DashboardContent />
</Suspense>
);
}
10 changes: 5 additions & 5 deletions frontend/src/hooks/use-communities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ export function useCommunities() {
const fetchCommunities = async () => {
try {
setIsLoading(true);
// Assuming backend exposes a route to get public/discoverable communities
const response = await apiClient.get<Community[]>("/communities");
setCommunities(response.data);
const response = await apiClient.get<Community[]>('/communities');
setCommunities(response.data ?? []);
setError(null);
} catch (err: any) {
console.error("Failed to fetch communities", err);
setError(err);
console.warn('Failed to fetch communities, using empty state', err?.message || err);
setCommunities([]);
setError(null);
Comment on lines 33 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't clear error state on fetch failure — it breaks downstream error UIs.

Resetting error to null in the catch block means the hook's error state is always null, so consumers can no longer distinguish "no communities" from "fetch failed."

DiscoverPage renders a spinner while loading, shows an error panel only when error is set, otherwise shows "No communities found" when filteredCommunities is empty. With this change, backend/network failures will silently render as "The directory is currently empty," hiding real failures from users.

🐛 Proposed fix to preserve error visibility
       } catch (err: any) {
         console.warn('Failed to fetch communities, using empty state', err?.message || err);
         setCommunities([]);
-        setError(null);
+        setError(err instanceof Error ? err : new Error(err?.message || 'Failed to fetch communities'));
       } finally {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err: any) {
console.error("Failed to fetch communities", err);
setError(err);
console.warn('Failed to fetch communities, using empty state', err?.message || err);
setCommunities([]);
setError(null);
} catch (err: any) {
console.warn('Failed to fetch communities, using empty state', err?.message || err);
setCommunities([]);
setError(err instanceof Error ? err : new Error(err?.message || 'Failed to fetch communities'));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/use-communities.ts` around lines 33 - 36, The catch block
in useCommunities is clearing the hook’s error state, which hides fetch failures
from consumers like DiscoverPage. Update the error handling in useCommunities so
the catch path preserves the failure by setting error to the caught exception
(or a derived message) instead of resetting it to null, while still clearing
communities as needed; keep the existing loading/reset flow in the hook’s fetch
logic intact.

} finally {
setIsLoading(false);
}
Expand Down