added more

This commit is contained in:
Sanjib Sen 2025-01-15 15:00:54 +06:00
parent 2c625d43a6
commit 21277a1aeb
8 changed files with 274 additions and 13 deletions

5
.gitignore vendored
View file

@ -40,4 +40,7 @@ yarn-error.log*
**/*.tgz
**/*.log
package-lock.json
**/*.bun
**/*.bun
./server
./.storage_data

View file

@ -13,8 +13,8 @@ RUN bun install --frozen-lockfile
# Copy source code
COPY . .
# Build the application
RUN bun build ./src/index.ts --compile --outfile server
# RUN DB Migrations and build
RUN bun run db:migrateb && bun run build
# Production stage
FROM debian:bookworm-slim
@ -28,4 +28,4 @@ COPY --from=builder /app/server .
EXPOSE 3000
# Run the binary
CMD ["./server"]
CMD ["./server"]

BIN
bun.lockb

Binary file not shown.

View file

@ -5,13 +5,11 @@ services:
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- .env
environment:
- NODE_ENV=production
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tracing:4318
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
NODE_ENV: production
OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
DATABASE_URL: ${DATABASE_URL}
networks:
- api-network
depends_on:
@ -20,8 +18,8 @@ services:
tracing:
image: jaegertracing/all-in-one:latest
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
- COLLECTOR_OTLP_ENABLED=true
COLLECTOR_ZIPKIN_HOST_PORT: 9411
COLLECTOR_OTLP_ENABLED: true
ports:
- "16686:16686"
networks:
@ -42,9 +40,31 @@ services:
networks:
- api-network
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 30s
timeout: 20s
retries: 3
networks:
- api-network
networks:
api-network:
driver: bridge
volumes:
postgres_data:
minio_data:

View file

@ -6,6 +6,7 @@
"dev": "bun run --watch src/index.ts",
"email": "email dev --dir src/emails",
"db:studio": "drizzle-kit studio",
"db:check": "drizzle-kit check",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
@ -22,6 +23,7 @@
"drizzle-orm": "^0.38.3",
"drizzle-typebox": "^0.2.1",
"elysia": "latest",
"minio": "^8.0.3",
"nodemailer": "^6.9.16",
"pg": "^8.13.1",
"react": "^19.0.0",

View file

@ -6,6 +6,7 @@ import { cors } from '@elysiajs/cors'
import { note } from "./api/note/note.route";
import { betterAuthView } from "./lib/auth/auth-view";
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
import { validateEnv } from "./lib/utils/env";
const app = new Elysia()
.use(cors())
@ -23,6 +24,6 @@ const app = new Elysia()
.use(note)
.get("/user", ({ user, session }) => userInfo(user, session));
validateEnv();
app.listen(3000);
console.log("Server is running on: http://localhost:3000")

143
src/lib/storage/s3.ts Normal file
View file

@ -0,0 +1,143 @@
import { Client } from 'minio';
import { Buffer } from 'buffer';
import { getMinioConfig } from '../utils/env';
// MinIO client configuration
const minioConfig = getMinioConfig()
const minioClient = new Client({
endPoint: minioConfig.MINIO_ENDPOINT_URL,
useSSL: false,
accessKey: minioConfig.MINIO_ACCESS_KEY,
secretKey: minioConfig.MINIO_SECRET_KEY
});
const BUCKET_NAME = minioConfig.MINIO_BUCKET_NAME;
const SIGNED_URL_EXPIRY = 24 * 60 * 60; // 24 hours in seconds
// Ensure bucket exists
const ensureBucket = async (): Promise<void> => {
const bucketExists = await minioClient.bucketExists(BUCKET_NAME);
if (!bucketExists) {
await minioClient.makeBucket(BUCKET_NAME);
}
};
/**
* Convert File/Blob to Buffer
* @param file - File or Blob object
* @returns Promise<Buffer>
*/
const fileToBuffer = async (file: File | Blob): Promise<Buffer> => {
const arrayBuffer = await file.arrayBuffer();
return Buffer.from(arrayBuffer);
};
/**
* Upload a file to MinIO and return its signed URL
* Supports both Buffer and File/Blob (from multipart form-data)
* @param filename - The name to store the file as
* @param file - The file to upload (Buffer, File, or Blob)
* @param contentType - Optional MIME type of the file
* @returns Promise with the signed URL of the uploaded file
*/
export const uploadFileAndGetUrl = async (
filename: string,
file: Buffer | File | Blob,
contentType?: string
): Promise<string> => {
try {
await ensureBucket();
// Convert File/Blob to Buffer if needed
const fileBuffer = Buffer.isBuffer(file) ? file : await fileToBuffer(file);
// If file is from form-data and no contentType is provided, use its type
const metadata: Record<string, string> = {};
if (!contentType && 'type' in file) {
contentType = file.type;
}
if (contentType) {
metadata['Content-Type'] = contentType;
}
// Upload the file
await minioClient.putObject(
BUCKET_NAME,
filename,
fileBuffer,
fileBuffer.length,
metadata
);
// Generate and return signed URL
const url = await minioClient.presignedGetObject(
BUCKET_NAME,
filename,
SIGNED_URL_EXPIRY
);
return url;
} catch (error) {
console.error('Error uploading file:', error);
if (error instanceof Error) {
throw new Error(`Failed to upload file: ${error.message}`);
}
throw new Error(`Failed to upload file: ${error}`);
}
};
/**
* Get a signed URL for an existing file
* @param filename - The name of the file to get URL for
* @returns Promise with the signed URL
*/
export const getSignedUrl = async (filename: string): Promise<string> => {
try {
// Check if file exists first
await minioClient.statObject(BUCKET_NAME, filename);
const url = await minioClient.presignedGetObject(
BUCKET_NAME,
filename,
SIGNED_URL_EXPIRY
);
return url;
} catch (error) {
console.error('Error generating signed URL:', error);
if (error instanceof Error) throw new Error(`Failed to generate signed URL: ${error.message}`);
throw new Error(`Failed to generate signed URL: ${error}`);
}
};
/**
* Delete a file from MinIO
* @param filename - The name of the file to delete
* @returns Promise<void>
*/
export const deleteFile = async (filename: string): Promise<void> => {
try {
await minioClient.removeObject(BUCKET_NAME, filename);
} catch (error) {
console.error('Error deleting file:', error);
if (error instanceof Error) throw new Error(`Failed to delete file: ${error.message}`);
throw new Error(`Failed to delete file: ${error}`);
}
};
// Usage examples:
// Upload a file and get its URL
// const fileBuffer = Buffer.from('Hello World');
// const url = await uploadFileAndGetUrl('hello.txt', fileBuffer);
// console.log('Uploaded file URL:', url);
// Get signed URL for existing file
// const url = await getSignedUrl('hello.txt');
// console.log('Signed URL:', url);
// Delete a file
// await deleteFile('hello.txt');
// console.log('File deleted successfully');

92
src/lib/utils/env.ts Normal file
View file

@ -0,0 +1,92 @@
import { z } from 'zod';
// Define the environment schema
const envSchema = z.object({
// Database
DATABASE_URL: z.string().url(),
// MinIO
MINIO_ACCESS_KEY: z.string(),
MINIO_SECRET_KEY: z.string().min(8),
MINIO_ENDPOINT_URL: z.string().url(),
MINIO_BUCKET_NAME: z.string().url(),
});
// Create a type from the schema
type EnvConfig = z.infer<typeof envSchema>;
/**
* Validates environment variables and returns a typed config object
* Prints warnings for missing or invalid variables
*/
export const validateEnv = (): EnvConfig => {
const warnings: string[] = [];
// Parse environment with warning collection
const config = envSchema.safeParse(process.env);
if (!config.success) {
console.warn('\n🚨 Environment Variable Warnings:');
// Collect and categorize warnings
config.error.errors.forEach((error) => {
const path = error.path.join('.');
const message = error.message;
let warningMessage = `${path}: ${message}`;
// Add specific functionality warnings
if (path.startsWith('DB_') || path === 'DATABASE_URL') {
warningMessage += '\n ⚠️ Database functionality may not work properly';
}
if (path.startsWith('MINIO_')) {
warningMessage += '\n ⚠️ File storage functionality may not work properly';
}
warnings.push(warningMessage);
});
// Print all warnings
warnings.forEach((warning) => console.warn(warning));
console.warn('\n');
throw new Error('Environment validation failed. Check warnings above.');
}
return config.data;
};
/**
* Get validated environment config
* Throws error if validation fails
*/
export const getConfig = (): EnvConfig => {
return validateEnv();
};
// Optional: Export individual config getters with type safety
export const getDbConfig = (): Pick<EnvConfig, 'DATABASE_URL'> => {
const config = getConfig();
return {
DATABASE_URL: config.DATABASE_URL,
};
};
export const getMinioConfig = (): Pick<EnvConfig, 'MINIO_ACCESS_KEY' | 'MINIO_SECRET_KEY' | 'MINIO_ENDPOINT_URL' | 'MINIO_BUCKET_NAME'> => {
const config = getConfig();
return {
MINIO_ACCESS_KEY: config.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: config.MINIO_SECRET_KEY,
MINIO_ENDPOINT_URL: config.MINIO_ENDPOINT_URL,
MINIO_BUCKET_NAME: config.MINIO_BUCKET_NAME,
};
};
// Usage example:
try {
const config = getConfig();
// Your application code here
} catch (error) {
// Handle validation errors
process.exit(1);
}