added more
This commit is contained in:
parent
2c625d43a6
commit
21277a1aeb
8 changed files with 274 additions and 13 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -41,3 +41,6 @@ yarn-error.log*
|
||||||
**/*.log
|
**/*.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
**/*.bun
|
**/*.bun
|
||||||
|
|
||||||
|
./server
|
||||||
|
./.storage_data
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ RUN bun install --frozen-lockfile
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application
|
# RUN DB Migrations and build
|
||||||
RUN bun build ./src/index.ts --compile --outfile server
|
RUN bun run db:migrateb && bun run build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
|
||||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -5,13 +5,11 @@ services:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
NODE_ENV: production
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tracing:4318
|
OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318
|
||||||
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
|
||||||
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
networks:
|
networks:
|
||||||
- api-network
|
- api-network
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -20,8 +18,8 @@ services:
|
||||||
tracing:
|
tracing:
|
||||||
image: jaegertracing/all-in-one:latest
|
image: jaegertracing/all-in-one:latest
|
||||||
environment:
|
environment:
|
||||||
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
|
COLLECTOR_ZIPKIN_HOST_PORT: 9411
|
||||||
- COLLECTOR_OTLP_ENABLED=true
|
COLLECTOR_OTLP_ENABLED: true
|
||||||
ports:
|
ports:
|
||||||
- "16686:16686"
|
- "16686:16686"
|
||||||
networks:
|
networks:
|
||||||
|
|
@ -42,9 +40,31 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- api-network
|
- 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:
|
networks:
|
||||||
api-network:
|
api-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "bun run --watch src/index.ts",
|
||||||
"email": "email dev --dir src/emails",
|
"email": "email dev --dir src/emails",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:check": "drizzle-kit check",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
|
"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-orm": "^0.38.3",
|
||||||
"drizzle-typebox": "^0.2.1",
|
"drizzle-typebox": "^0.2.1",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
|
"minio": "^8.0.3",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { cors } from '@elysiajs/cors'
|
||||||
import { note } from "./api/note/note.route";
|
import { note } from "./api/note/note.route";
|
||||||
import { betterAuthView } from "./lib/auth/auth-view";
|
import { betterAuthView } from "./lib/auth/auth-view";
|
||||||
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
|
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
|
||||||
|
import { validateEnv } from "./lib/utils/env";
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(cors())
|
.use(cors())
|
||||||
|
|
@ -23,6 +24,6 @@ const app = new Elysia()
|
||||||
.use(note)
|
.use(note)
|
||||||
.get("/user", ({ user, session }) => userInfo(user, session));
|
.get("/user", ({ user, session }) => userInfo(user, session));
|
||||||
|
|
||||||
|
validateEnv();
|
||||||
app.listen(3000);
|
app.listen(3000);
|
||||||
|
|
||||||
console.log("Server is running on: http://localhost:3000")
|
console.log("Server is running on: http://localhost:3000")
|
||||||
|
|
|
||||||
143
src/lib/storage/s3.ts
Normal file
143
src/lib/storage/s3.ts
Normal 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
92
src/lib/utils/env.ts
Normal 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);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue