added more
This commit is contained in:
parent
2c625d43a6
commit
21277a1aeb
8 changed files with 274 additions and 13 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -40,4 +40,7 @@ yarn-error.log*
|
|||
**/*.tgz
|
||||
**/*.log
|
||||
package-lock.json
|
||||
**/*.bun
|
||||
**/*.bun
|
||||
|
||||
./server
|
||||
./.storage_data
|
||||
|
|
|
|||
|
|
@ -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
BIN
bun.lockb
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
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