Skip to content
Merged
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ Me personally, after HC's neighborhood ended, I just felt the need of a platform

## What does this project use?
THis project uses a lot of libraries (and frameworks)
- Next.js
- Next.js 16
- Tailwind CSS
- Better-Auth
- Drizzle
- Postgres
- Docker
- Bun
- shadcn/ui
- Sonnar

## Currently known issues
Most of the known issues are in GH issues (as a issue tracker)
But some of it I'm going to list here:
- The admin toggles will just move around for some reason
- The edit system is still broken

## Auth
You must pull the GitHub Repo in order to recompile it. This guide assumes that you've using Linux and already got bun (or npm, yarn, pnpm, deno), git and docker buildx installed.
Expand Down
28 changes: 14 additions & 14 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,43 @@
"appi": "sfw bun install"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.913.0",
"@aws-sdk/lib-storage": "^3.913.0",
"@aws-sdk/s3-request-presigner": "^3.913.0",
"@aws-sdk/client-s3": "^3.930.0",
"@aws-sdk/lib-storage": "^3.930.0",
"@aws-sdk/s3-request-presigner": "^3.930.0",
"@devlogs_hosting/auth": "workspace:*",
"@geist-ui/core": "^2.3.8",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-form": "^1.23.7",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-form": "^1.23.8",
"@tanstack/react-query": "^5.90.8",
"@tanstack/react-table": "^8.21.3",
"better-auth": "^1.3.28",
"better-auth": "^1.3.34",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.6",
"drizzle-orm": "^0.44.7",
"geist": "^1.5.1",
"lucide-react": "^0.546.0",
"next": "15.5.4",
"next": "^16.0.2",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.14",
"@types/node": "^20.19.22",
"@tailwindcss/postcss": "^4.1.17",
"@types/node": "^20.19.25",
"@types/react": "~19.1.17",
"@types/react-dom": "^19.2.2",
"tailwindcss": "^4.1.14",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"@tanstack/react-query-devtools": "^5.90.2"
}
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/app/api/data/files/[...slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ type Props = {

export const GET = async (request: NextRequest, context: Props) => {
try {
// Check if S3 is configured
if (!s3.s3Config.isConfigured) {
return new Response(
JSON.stringify({ error: "S3 storage not configured" }),
{
status: 503,
headers: { "Content-Type": "application/json" },
},
);
}

const { slug } = await context.params;
let buildUrl: string = "";
for (let i = 0; i < slug.length; i++) {
Expand Down
16 changes: 11 additions & 5 deletions apps/web/src/app/api/data/publish/file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,18 @@ export const POST = async (request: NextRequest) => {
);
}

// Validate required environment variables
if (!process.env.S3_BUCKET_NAME) {
console.error("S3_BUCKET_NAME environment variable is not set");
// Check if S3 is configured
if (!s3.s3Config.isConfigured) {
console.error(
"S3 is not configured - missing required environment variables",
);
return Response.json(
{ success: false, msg: "Server configuration error", uploadUrl: "" },
{ status: 500 },
{
success: false,
msg: "File upload is not available - S3 storage not configured",
uploadUrl: "",
},
{ status: 503 },
);
}

Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/api/modify/posts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextRequest } from "next/server";

export const POST = async (request: NextRequest) => {
return Response.json({});
};
108 changes: 89 additions & 19 deletions apps/web/src/lib/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import generateId from "./generate_id";

// Validate required environment variables
/**
* Determine whether the required S3 environment variables are present.
*
* @returns `true` if `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, and `S3_BUCKET_NAME` are all set in the environment, `false` otherwise.
*/
function isS3Configured(): boolean {
return !!(
process.env.S3_ACCESS_KEY_ID &&
process.env.S3_SECRET_ACCESS_KEY &&
process.env.S3_BUCKET_NAME
);
}

/**
* Ensures required S3 environment variables are present.
*
* Throws an error listing any missing variables.
*
* @throws Error If one or more required environment variables are not set, with a message listing the missing keys.
*/
function validateS3Config() {
const required = [
"S3_ACCESS_KEY_ID",
Expand All @@ -19,28 +38,68 @@ function validateS3Config() {
}
}

// Validate config on import
validateS3Config();
// Lazy initialization of S3 client
let _s3Client: S3Client | null = null;

// Initialize S3 client with better configuration
export const s3Client = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "auto",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
/**
* Get the singleton S3 client, initializing and validating configuration on first use.
*
* @returns The initialized `S3Client` instance.
* @throws Error if required S3 environment variables are missing or S3 is not configured.
*/
export function getS3Client(): S3Client {
if (!isS3Configured()) {
throw new Error(
"S3 is not configured. Please set S3 environment variables.",
);
}

if (!_s3Client) {
validateS3Config();

_s3Client = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "auto",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
// Add retry configuration
maxAttempts: 3,
// Force path style for compatibility with some S3 providers
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
});
}

return _s3Client;
}

// Export for backward compatibility, but will throw if S3 is not configured
export const s3Client = new Proxy({} as S3Client, {
get(target, prop) {
const client = getS3Client();
const value = (client as any)[prop];
return typeof value === "function" ? value.bind(client) : value;
},
// Add retry configuration
maxAttempts: 3,
// Force path style for compatibility with some S3 providers
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
});

export const s3Config = {
client: s3Client,
bucket: process.env.S3_BUCKET_NAME!,
get client() {
return getS3Client();
},
get bucket() {
if (!isS3Configured()) {
throw new Error(
"S3 is not configured. Please set S3_BUCKET_NAME environment variable.",
);
}
return process.env.S3_BUCKET_NAME!;
},
region: process.env.S3_REGION || "us-east-1",
endpoint: process.env.S3_ENDPOINT,
get isConfigured() {
return isS3Configured();
},
};

// Generate a unique file name with better validation
Expand All @@ -64,13 +123,24 @@ export function generateFileName(originalName: string): string {
return `uploads/${timestamp}_${randomId}.${extension}`;
}

// Helper function to get signed URL for secure uploads (if needed)
/**
* Create a presigned PUT URL for uploading an object to the configured S3 bucket.
*
* @param key - The destination object key (path) within the S3 bucket.
* @param contentType - The MIME type that will be set for the uploaded object.
* @returns A signed URL that allows uploading the object to S3.
* @throws Error if S3 is not configured and a signed URL cannot be generated.
*/
export async function getSignedUploadUrl(key: string, contentType: string) {
if (!isS3Configured()) {
throw new Error("S3 is not configured. Cannot generate signed upload URL.");
}

const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: key,
ContentType: contentType,
});

return await getSignedUrl(s3Client, command, { expiresIn: 3600 }); // 1 hour
}
return await getSignedUrl(getS3Client(), command, { expiresIn: 3600 }); // 1 hour
}
5 changes: 3 additions & 2 deletions apps/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -34,7 +34,8 @@
"./**/*.tsx",
"./.next/types/**/*.ts",
"./next-env.d.ts",
".next/types/**/*.ts"
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"./node_modules"
Expand Down
Loading