migration to typescript
This commit is contained in:
parent
924000b084
commit
9fde77468a
41 changed files with 11952 additions and 10164 deletions
|
|
@ -1,51 +0,0 @@
|
|||
const { PrismaClient } = require('@prisma/client');
|
||||
const logger = require('./logger');
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: [
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'query',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'error',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'info',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Log Prisma queries in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
prisma.$on('query', (e) => {
|
||||
logger.debug(`Query: ${e.query}`);
|
||||
logger.debug(`Duration: ${e.duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
prisma.$on('error', (e) => {
|
||||
logger.error(`Prisma Error: ${e.message}`);
|
||||
});
|
||||
|
||||
prisma.$on('warn', (e) => {
|
||||
logger.warn(`Prisma Warning: ${e.message}`);
|
||||
});
|
||||
|
||||
// Test connection
|
||||
prisma.$connect()
|
||||
.then(() => {
|
||||
logger.info('✅ Database connected successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('❌ Database connection failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.exports = prisma;
|
||||
40
Backend/src/config/database.ts
Normal file
40
Backend/src/config/database.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { logger } from './logger';
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: [
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'query',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'error',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Log queries in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
prisma.$on('query' as never, (e: any) => {
|
||||
logger.debug('Prisma Query', {
|
||||
query: e.query,
|
||||
params: e.params,
|
||||
duration: `${e.duration}ms`
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prisma.$on('error' as never, (e: any) => {
|
||||
logger.error('Prisma Error', { error: e });
|
||||
});
|
||||
|
||||
prisma.$on('warn' as never, (e: any) => {
|
||||
logger.warn('Prisma Warning', { warning: e });
|
||||
});
|
||||
|
||||
export { prisma };
|
||||
75
Backend/src/config/index.ts
Normal file
75
Backend/src/config/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Validate required environment variables
|
||||
const requiredEnvVars = [
|
||||
'DATABASE_URL',
|
||||
'JWT_SECRET',
|
||||
'PORT'
|
||||
];
|
||||
|
||||
requiredEnvVars.forEach(key => {
|
||||
if (!process.env[key]) {
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
// Application
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT || '4000', 10),
|
||||
appUrl: process.env.APP_URL || 'http://localhost:4000',
|
||||
|
||||
// Database
|
||||
databaseUrl: process.env.DATABASE_URL!,
|
||||
|
||||
// Redis
|
||||
redis: {
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
password: process.env.REDIS_PASSWORD
|
||||
},
|
||||
|
||||
// MinIO/S3
|
||||
s3: {
|
||||
endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000',
|
||||
accessKey: process.env.S3_ACCESS_KEY || 'minioadmin',
|
||||
secretKey: process.env.S3_SECRET_KEY || 'minioadmin',
|
||||
bucket: process.env.S3_BUCKET || 'e-learning',
|
||||
useSSL: process.env.S3_USE_SSL === 'true'
|
||||
},
|
||||
|
||||
// JWT
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET!,
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d'
|
||||
},
|
||||
|
||||
// Email
|
||||
smtp: {
|
||||
host: process.env.SMTP_HOST || 'localhost',
|
||||
port: parseInt(process.env.SMTP_PORT || '1025', 10),
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
from: process.env.SMTP_FROM || 'noreply@elearning.local'
|
||||
},
|
||||
|
||||
// File Upload
|
||||
upload: {
|
||||
maxVideoSize: parseInt(process.env.MAX_VIDEO_SIZE || '524288000', 10), // 500MB
|
||||
maxAttachmentSize: parseInt(process.env.MAX_ATTACHMENT_SIZE || '104857600', 10), // 100MB
|
||||
maxAttachmentsPerLesson: parseInt(process.env.MAX_ATTACHMENTS_PER_LESSON || '10', 10)
|
||||
},
|
||||
|
||||
// CORS
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||
},
|
||||
|
||||
// Rate Limiting
|
||||
rateLimit: {
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes
|
||||
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10)
|
||||
}
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
const winston = require('winston');
|
||||
|
||||
const logLevels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
const logColors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
http: 'magenta',
|
||||
debug: 'blue',
|
||||
};
|
||||
|
||||
winston.addColors(logColors);
|
||||
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(
|
||||
(info) => `${info.timestamp} ${info.level}: ${info.message}`
|
||||
)
|
||||
);
|
||||
|
||||
const transports = [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
}),
|
||||
new winston.transports.File({ filename: 'logs/all.log' }),
|
||||
];
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
levels: logLevels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
44
Backend/src/config/logger.ts
Normal file
44
Backend/src/config/logger.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import winston from 'winston';
|
||||
import { config } from './index';
|
||||
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.splat(),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
let msg = `${timestamp} [${level}]: ${message}`;
|
||||
if (Object.keys(meta).length > 0) {
|
||||
msg += ` ${JSON.stringify(meta)}`;
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
);
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: config.nodeEnv === 'production' ? 'info' : 'debug',
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/combined.log'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Don't log to files in development
|
||||
if (config.nodeEnv !== 'production') {
|
||||
logger.clear();
|
||||
logger.add(new winston.transports.Console({ format: consoleFormat }));
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
const redis = require('redis');
|
||||
const logger = require('./logger');
|
||||
|
||||
let redisClient = null;
|
||||
|
||||
async function connectRedis() {
|
||||
try {
|
||||
redisClient = redis.createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
});
|
||||
|
||||
redisClient.on('error', (err) => {
|
||||
logger.error('Redis Client Error:', err);
|
||||
});
|
||||
|
||||
redisClient.on('connect', () => {
|
||||
logger.info('✅ Redis connected successfully');
|
||||
});
|
||||
|
||||
await redisClient.connect();
|
||||
return redisClient;
|
||||
} catch (error) {
|
||||
logger.error('❌ Redis connection failed:', error);
|
||||
// Don't exit, allow app to run without Redis
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRedisClient() {
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connectRedis,
|
||||
getRedisClient,
|
||||
};
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
const swaggerJsdoc = require('swagger-jsdoc');
|
||||
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'E-Learning Platform API',
|
||||
version: '1.0.0',
|
||||
description: 'API documentation for E-Learning Platform Backend',
|
||||
contact: {
|
||||
name: 'API Support',
|
||||
email: 'support@elearning.local',
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT',
|
||||
},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: process.env.APP_URL || 'http://localhost:4000',
|
||||
description: 'Development server',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'Enter your JWT token',
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
example: 'VALIDATION_ERROR',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'Invalid input data',
|
||||
},
|
||||
details: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
field: {
|
||||
type: 'string',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
example: 1,
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
example: 'john_doe',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'john@example.com',
|
||||
},
|
||||
role_id: {
|
||||
type: 'integer',
|
||||
example: 3,
|
||||
},
|
||||
is_active: {
|
||||
type: 'boolean',
|
||||
example: true,
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
},
|
||||
role: {
|
||||
$ref: '#/components/schemas/Role',
|
||||
},
|
||||
profile: {
|
||||
$ref: '#/components/schemas/Profile',
|
||||
},
|
||||
},
|
||||
},
|
||||
Role: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
example: 3,
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
example: 'STUDENT',
|
||||
},
|
||||
name: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
th: {
|
||||
type: 'string',
|
||||
example: 'นักเรียน',
|
||||
},
|
||||
en: {
|
||||
type: 'string',
|
||||
example: 'Student',
|
||||
},
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
th: {
|
||||
type: 'string',
|
||||
},
|
||||
en: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Profile: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
},
|
||||
user_id: {
|
||||
type: 'integer',
|
||||
},
|
||||
first_name: {
|
||||
type: 'string',
|
||||
example: 'John',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
example: 'Doe',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
example: '+66812345678',
|
||||
},
|
||||
avatar_url: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
},
|
||||
bio: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
th: {
|
||||
type: 'string',
|
||||
},
|
||||
en: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: {
|
||||
$ref: '#/components/schemas/User',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
},
|
||||
refreshToken: {
|
||||
type: 'string',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: [
|
||||
{
|
||||
name: 'Authentication',
|
||||
description: 'User authentication and authorization endpoints',
|
||||
},
|
||||
{
|
||||
name: 'Health',
|
||||
description: 'System health check endpoints',
|
||||
},
|
||||
],
|
||||
},
|
||||
apis: ['./src/routes/*.js', './src/app.js'],
|
||||
};
|
||||
|
||||
const swaggerSpec = swaggerJsdoc(options);
|
||||
|
||||
module.exports = swaggerSpec;
|
||||
Loading…
Add table
Add a link
Reference in a new issue