first commit
This commit is contained in:
commit
f5639c6615
|
@ -0,0 +1,15 @@
|
||||||
|
/.idea/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
uploads/*
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
cloud_sql_proxy
|
||||||
|
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
bin/build
|
||||||
|
|
||||||
|
ngrok
|
||||||
|
|
||||||
|
.DS_Store
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM node:14
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
# If you are building your code for production
|
||||||
|
#RUN npm ci --only=production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5698
|
||||||
|
CMD [ "node", "./bin/www" ]
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const app = require('./build/app');
|
||||||
|
const debug = require('debug')('order-services-node:server');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port from environment and store in Express.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const port = normalizePort(process.env.PORT || '3000');
|
||||||
|
app.set('port', port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HTTP server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen on provided port, on all network interfaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
server.listen(port);
|
||||||
|
server.on('error', onError);
|
||||||
|
server.on('listening', onListening);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a port into a number, string, or false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizePort(val) {
|
||||||
|
const port = parseInt(val, 10);
|
||||||
|
|
||||||
|
if (isNaN(port)) {
|
||||||
|
// named pipe
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port >= 0) {
|
||||||
|
// port number
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "error" event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
if (error.syscall !== 'listen') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bind = typeof port === 'string'
|
||||||
|
? 'Pipe ' + port
|
||||||
|
: 'Port ' + port;
|
||||||
|
|
||||||
|
// handle specific listen errors with friendly messages
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
console.error(bind + ' requires elevated privileges');
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
console.error(bind + ' is already in use');
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "listening" event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onListening() {
|
||||||
|
const addr = server.address();
|
||||||
|
const bind = typeof addr === 'string'
|
||||||
|
? 'pipe ' + addr
|
||||||
|
: 'port ' + addr.port;
|
||||||
|
debug('Listening on ' + bind);
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "order-services-node",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./bin/www",
|
||||||
|
"dev": "npx ts-node-dev --respawn --transpile-only ./src/server.ts",
|
||||||
|
"preview": "npx ts-node-dev ./bin/build/app.ts",
|
||||||
|
"build": "tsc --build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"amqplib": "^0.10.3",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cookie-parser": "~1.4.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"debug": "~2.6.9",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"ejs": "^3.1.9",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-validator": "^7.2.1",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"http-errors": "~1.6.3",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"knex": "^2.5.1",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mongodb": "^6.14.0",
|
||||||
|
"mongoose": "^8.11.0",
|
||||||
|
"morgan": "~1.9.1",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"mysql2": "^3.14.1",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"nodemailer": "^6.10.0",
|
||||||
|
"nodemon": "^3.1.9",
|
||||||
|
"pg": "^8.11.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/cookie-parser": "^1.4.8",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// src/app.ts
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import logger from 'morgan';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import compression from 'compression';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
// import internalRouter from './routes/internal';
|
||||||
|
import apiRouter from './routes/api';
|
||||||
|
import connectDB from './database/MySQL';
|
||||||
|
import { root_path } from './utils/core/storage';
|
||||||
|
import { respondWithError } from './middleware/core/errorHandler';
|
||||||
|
// import schedule from 'node-schedule';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin(origin, callback) {
|
||||||
|
if (!origin) return callback(null, true);
|
||||||
|
|
||||||
|
if (allowedOrigins.includes(origin)) {
|
||||||
|
return callback(null, true);
|
||||||
|
} else {
|
||||||
|
return callback(new Error('Not allowed by CORS'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(compression());
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(logger('dev'));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// View engine setup
|
||||||
|
// app.set('views', path.join(__dirname, 'views'));
|
||||||
|
app.set('views', root_path('src/views'));
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
connectDB();
|
||||||
|
|
||||||
|
app.get('/', function (req, res, next) {
|
||||||
|
res.json({
|
||||||
|
status: 'running',
|
||||||
|
developer: 'Fahim',
|
||||||
|
project: 'Stock Prediction with ARIMA'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
// 404 handler (opsional, bisa diaktifkan jika perlu)
|
||||||
|
// app.use((_req, _res, next) => {
|
||||||
|
// next(createHttpError(404));
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Scheduler (opsional, jika ingin dijalankan otomatis)
|
||||||
|
// const scheduleMidnight = schedule.scheduleJob('0 0 * * *', async () => {
|
||||||
|
// const result = await runningSchedule() ? 'success running midnight schedule' : 'failed running midnight schedule';
|
||||||
|
// console.log(result);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const scheduleNoon = schedule.scheduleJob('0 12 * * *', async () => {
|
||||||
|
// const result = await runningSchedule() ? 'success running noon schedule' : 'failed running noon schedule';
|
||||||
|
// console.log(result);
|
||||||
|
// });
|
||||||
|
|
||||||
|
app.use(respondWithError);
|
||||||
|
|
||||||
|
export default app;
|
|
@ -0,0 +1,394 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import jwt, { VerifyCallback } from 'jsonwebtoken';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
|
||||||
|
import createHttpError from 'http-errors';
|
||||||
|
import { generateAccessToken, generateRefreshToken } from '../../utils/api/generateCredentialToken';
|
||||||
|
|
||||||
|
import { body, matchedData, param } from 'express-validator';
|
||||||
|
import expressValidatorErrorHandler from '../../middleware/expressValidatorErrorHandler';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import { createUser } from '../../repository/usersRepository';
|
||||||
|
import { activateUser, getUserByEmail, getUserByRefreshToken, updateUserById } from '../../services/userServices';
|
||||||
|
import { TAPIResponse } from '../../types/core/http';
|
||||||
|
|
||||||
|
const saltRounds = 10;
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
service: 'gmail',
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAccountActivationToken = {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = [
|
||||||
|
body('email')
|
||||||
|
.notEmpty().withMessage('Email is required')
|
||||||
|
.normalizeEmail()
|
||||||
|
.isEmail().withMessage('Invalid email format'),
|
||||||
|
|
||||||
|
body('name')
|
||||||
|
.optional()
|
||||||
|
.isString().withMessage('Name must be a string')
|
||||||
|
.isLength({ min: 4, max: 20 }).withMessage('Name must be between 4 and 20 characters'),
|
||||||
|
|
||||||
|
body('password')
|
||||||
|
.notEmpty().withMessage('Password is required')
|
||||||
|
.isStrongPassword({ minLength: 8 }).withMessage('Password must be strong (min 8 chars, uppercase, lowercase, number, and symbol)'),
|
||||||
|
|
||||||
|
body('emailActivationPage')
|
||||||
|
.optional().isString(),
|
||||||
|
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { email, name, password, emailActivationPage = process.env.HOST } = matchedData(req);
|
||||||
|
|
||||||
|
// Cek apakah user sudah terdaftar
|
||||||
|
const existingUser = await getUserByEmail(email);
|
||||||
|
if (existingUser) return next(createHttpError(400, "Email already exists"));
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
|
// Buat token verifikasi
|
||||||
|
const activationToken = jwt.sign({ email } as TAccountActivationToken, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||||
|
|
||||||
|
// Simpan user
|
||||||
|
const newUser = await createUser({ email, name: name ?? email, password: hashedPassword });
|
||||||
|
|
||||||
|
// Kirim email verifikasi
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.EMAIL_USER,
|
||||||
|
to: email,
|
||||||
|
subject: "Verify Your Email",
|
||||||
|
text: `Click this link to verify your email: ${emailActivationPage}/${activationToken}`
|
||||||
|
};
|
||||||
|
|
||||||
|
transporter.sendMail(mailOptions, (err) => {
|
||||||
|
if (err) return next(createHttpError(500, "Error sending verification email", { cause: err }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: "User registered. Please check your email to verify your account. Please check your email inbox or spam inbox."
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(mailOptions)
|
||||||
|
}
|
||||||
|
res.status(201).json(resultResponse);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Error registering user", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const verify = [
|
||||||
|
param('token').isString().withMessage('Token must be string'),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { token } = matchedData(req);
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET) as TAccountActivationToken;
|
||||||
|
|
||||||
|
// Update status verifikasi
|
||||||
|
const user = await getUserByEmail(decoded.email)
|
||||||
|
|
||||||
|
if (!user) return next(createHttpError(400, "Invalid Token"));
|
||||||
|
|
||||||
|
await activateUser(decoded.email)
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: "Email verified successfully. You can now log in."
|
||||||
|
}
|
||||||
|
res.json(resultResponse);
|
||||||
|
} catch (err: any) {
|
||||||
|
next(createHttpError(400, "Invalid or expired token", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const resendEmailVerification = [
|
||||||
|
param('token').isString().withMessage('Token must be string'),
|
||||||
|
body('emailActivationPage')
|
||||||
|
.optional().isString(),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { token, emailActivationPage } = matchedData(req);
|
||||||
|
const decoded = jwt.decode(token) as TAccountActivationToken
|
||||||
|
const user = await getUserByEmail(decoded.email)
|
||||||
|
if (!user) return next(createHttpError(400, "Invalid Token"));
|
||||||
|
if (user.is_verified) return next(createHttpError(409, "This account has already been verified. You can log in directly."));
|
||||||
|
|
||||||
|
const activationToken = jwt.sign({ email: decoded.email } as TAccountActivationToken, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.EMAIL_USER,
|
||||||
|
to: decoded.email,
|
||||||
|
subject: "Verify Your Email",
|
||||||
|
text: `Click this link to verify your email: ${emailActivationPage}/${activationToken}`
|
||||||
|
};
|
||||||
|
|
||||||
|
transporter.sendMail(mailOptions, (err) => {
|
||||||
|
if (err) return next(createHttpError(500, "Error sending verification email", { cause: err }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: "Verification email has been sent successfully. Please check your email inbox or spam inbox."
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(mailOptions)
|
||||||
|
}
|
||||||
|
res.json(resultResponse)
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(400, "Invalid or expired token", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const login = [
|
||||||
|
body('email')
|
||||||
|
.notEmpty().withMessage('Email is required')
|
||||||
|
.normalizeEmail()
|
||||||
|
.isEmail().withMessage('Invalid email format'),
|
||||||
|
|
||||||
|
body('password')
|
||||||
|
.notEmpty().withMessage('Password is required'),
|
||||||
|
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = matchedData(req);
|
||||||
|
|
||||||
|
// Cari user
|
||||||
|
const user = await getUserByEmail(email);
|
||||||
|
if (!user) return next(createHttpError(400, "Invalid credentials"));
|
||||||
|
|
||||||
|
// Cek apakah email sudah diverifikasi
|
||||||
|
if (!user.is_verified) return next(createHttpError(400, "Please verify your email first"));
|
||||||
|
|
||||||
|
// Bandingkan password
|
||||||
|
const isMatch = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isMatch) {
|
||||||
|
return next(createHttpError(400, "Invalid credentials"))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buat tokens
|
||||||
|
const accessToken = generateAccessToken(user);
|
||||||
|
const refreshToken = generateRefreshToken(user);
|
||||||
|
|
||||||
|
// Simpan refresh token di database
|
||||||
|
await updateUserById(user.id, {
|
||||||
|
refresh_token: refreshToken
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kirim token sebagai cookie
|
||||||
|
res.cookie("refreshToken", refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 15 * 24 * 60 * 60 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: `Welcome back, ${user.name}`,
|
||||||
|
data: { accessToken }
|
||||||
|
}
|
||||||
|
res.json(resultResponse);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Error logging in", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const refreshToken = [
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const refreshToken = req.cookies.refreshToken;
|
||||||
|
if (!refreshToken) return next(createHttpError(401, "No refresh token provided"));
|
||||||
|
|
||||||
|
// Cari user dengan refresh token
|
||||||
|
const user = await getUserByRefreshToken(refreshToken);
|
||||||
|
if (!user) return next(createHttpError(403, "Invalid refresh token"));
|
||||||
|
|
||||||
|
const jwtVerifCB: VerifyCallback = (err, decoded) => {
|
||||||
|
if (err) return next(createHttpError(403, "Invalid refresh token"));
|
||||||
|
|
||||||
|
// Buat access token baru
|
||||||
|
const accessToken = generateAccessToken(user);
|
||||||
|
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
data: { accessToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(resultResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifikasi refresh token
|
||||||
|
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, jwtVerifCB);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Error refreshing token", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const logout = [
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const refreshToken = req.cookies.refreshToken;
|
||||||
|
if (!refreshToken) {
|
||||||
|
res.sendStatus(204);
|
||||||
|
return
|
||||||
|
} // Tidak ada token, langsung selesai
|
||||||
|
|
||||||
|
// Hapus refresh token dari database
|
||||||
|
await updateUserById(req.user!.id, {
|
||||||
|
refresh_token: null
|
||||||
|
})
|
||||||
|
|
||||||
|
res.clearCookie("refreshToken", { httpOnly: true, secure: process.env.NODE_ENV === "production" });
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: "Logged out successfully"
|
||||||
|
}
|
||||||
|
res.json(resultResponse);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Error logging out", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const forgotPasswordSendEmail = [
|
||||||
|
body('email')
|
||||||
|
.notEmpty().withMessage('Email is required')
|
||||||
|
.normalizeEmail()
|
||||||
|
.isEmail().withMessage('Invalid email format'),
|
||||||
|
|
||||||
|
body('emailActivationPage')
|
||||||
|
.optional().isString(),
|
||||||
|
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { email, emailActivationPage } = matchedData(req);
|
||||||
|
|
||||||
|
// Cari user
|
||||||
|
const user = await getUserByEmail(email);
|
||||||
|
if (user) {
|
||||||
|
if (!user.is_verified) return next(createHttpError(403, "Please verify your email first"));
|
||||||
|
|
||||||
|
const forgotPasswordToken = jwt.sign({ email } as TAccountActivationToken, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.EMAIL_USER,
|
||||||
|
to: email,
|
||||||
|
subject: "Forgot Password Link",
|
||||||
|
text: `Click this link to change your password: ${emailActivationPage}/${forgotPasswordToken}`
|
||||||
|
};
|
||||||
|
|
||||||
|
transporter.sendMail(mailOptions, (err) => {
|
||||||
|
if (err) return next(createHttpError(500, "Error sending forgot password link email", { cause: err }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: "Forgot password link has been sent successfully. Please check your email inbox or spam inbox."
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(mailOptions)
|
||||||
|
}
|
||||||
|
res.json(resultResponse);
|
||||||
|
} else {
|
||||||
|
return next(createHttpError(400, "Invalid credentials"))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Failed to generate forgot password token.", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const forgotPasswordVerifyToken = [
|
||||||
|
param('token').isString().withMessage('Token must be string'),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { token } = matchedData(req);
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET) as TAccountActivationToken;
|
||||||
|
|
||||||
|
// Update status verifikasi
|
||||||
|
const user = await getUserByEmail(decoded.email)
|
||||||
|
|
||||||
|
if (!user) return next(createHttpError(400, "Invalid Token"));
|
||||||
|
|
||||||
|
await activateUser(decoded.email)
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: "Token is Valid."
|
||||||
|
}
|
||||||
|
res.json(resultResponse);
|
||||||
|
} catch (err: any) {
|
||||||
|
next(createHttpError(400, "Invalid or expired token", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const forgotPasswordChangePassword = [
|
||||||
|
param('token').isString().withMessage('Token must be string'),
|
||||||
|
body('password')
|
||||||
|
.notEmpty().withMessage('Password is required')
|
||||||
|
.isStrongPassword({ minLength: 8 }).withMessage('Password must be strong (min 8 chars, uppercase, lowercase, number, and symbol)'),
|
||||||
|
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { token, password } = matchedData(req);
|
||||||
|
|
||||||
|
// Cari user
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET) as TAccountActivationToken;
|
||||||
|
const user = await getUserByEmail(decoded.email);
|
||||||
|
if (!user) return next(createHttpError(400, "User not found."));
|
||||||
|
|
||||||
|
// Cek apakah email sudah diverifikasi
|
||||||
|
if (!user.is_verified) return next(createHttpError(400, "Please verify your email first"));
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
|
// Simpan refresh token di database
|
||||||
|
await updateUserById(user.id, {
|
||||||
|
password: hashedPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultResponse: TAPIResponse = {
|
||||||
|
success: true,
|
||||||
|
message: `Password changed successful.`
|
||||||
|
}
|
||||||
|
res.json(resultResponse);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Error logging in", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
register,
|
||||||
|
verify, resendEmailVerification,
|
||||||
|
login,
|
||||||
|
forgotPasswordSendEmail, forgotPasswordVerifyToken, forgotPasswordChangePassword,
|
||||||
|
refreshToken,
|
||||||
|
logout,
|
||||||
|
};
|
|
@ -0,0 +1,94 @@
|
||||||
|
import express, { NextFunction, Request, Response } from 'express';
|
||||||
|
import { body, matchedData, param } from 'express-validator';
|
||||||
|
import createHttpError from 'http-errors';
|
||||||
|
import { product_categories } from '../../database/models';
|
||||||
|
import expressValidatorErrorHandler from '../../middleware/expressValidatorErrorHandler';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// POST: Tambah kategori baru
|
||||||
|
const createCategory = [
|
||||||
|
body("category_name").notEmpty().withMessage("Category name is required"),
|
||||||
|
body("description").optional().isString(),
|
||||||
|
body("user_id").notEmpty().withMessage("User ID is required").isMongoId(),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData(req);
|
||||||
|
const category = new product_categories(data);
|
||||||
|
await category.save();
|
||||||
|
res.status(201).json({ message: "Category created", category });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// PATCH: Perbarui kategori
|
||||||
|
const updateCategory = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid category ID"),
|
||||||
|
body("category_name").optional().isString(),
|
||||||
|
body("description").optional().isString(),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data = matchedData(req);
|
||||||
|
const category = await product_categories.findByIdAndUpdate(id, data, { new: true });
|
||||||
|
if (!category) return next(createHttpError(404, "Category not found"));
|
||||||
|
res.json({ message: "Category updated", category });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// DELETE: Hapus kategori
|
||||||
|
const deleteCategory = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid category ID"),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const category = await product_categories.findByIdAndDelete(id);
|
||||||
|
if (!category) return next(createHttpError(404, "Category not found"));
|
||||||
|
res.json({ message: "Category deleted" });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// GET: Ambil satu kategori berdasarkan ID
|
||||||
|
const getCategory = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid category ID"),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const category = await product_categories.findById(id);
|
||||||
|
if (!category) return next(createHttpError(404, "Category not found"));
|
||||||
|
res.json(category);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// GET: Ambil semua kategori
|
||||||
|
const getCategories = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const categories = await product_categories.find();
|
||||||
|
res.json(categories);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createCategory,
|
||||||
|
updateCategory,
|
||||||
|
deleteCategory,
|
||||||
|
getCategory,
|
||||||
|
getCategories,
|
||||||
|
};
|
|
@ -0,0 +1,129 @@
|
||||||
|
// controllers/productsController.ts
|
||||||
|
|
||||||
|
import express, { NextFunction, Request, Response } from "express";
|
||||||
|
import { body, param } from "express-validator";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { matchedData } from "express-validator";
|
||||||
|
import * as productCategoriesRepository from "../../repository/productCategories"; // Repository untuk Product Categories
|
||||||
|
import * as productsRepository from "../../repository/products"; // Repository untuk Products
|
||||||
|
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
|
||||||
|
|
||||||
|
// POST: Tambah produk baru
|
||||||
|
const createProduct = [
|
||||||
|
body("product_name").notEmpty().withMessage("Product name is required"),
|
||||||
|
body("description").optional().isString(),
|
||||||
|
body("price").notEmpty().isNumeric().withMessage("Price must be a number"),
|
||||||
|
body("product_category_id").notEmpty().isInt().withMessage("Invalid category ID"), // Ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
// Cek apakah kategori produk ada di database
|
||||||
|
const categoryExists = await productCategoriesRepository.getProductCategoryById(data.product_category_id);
|
||||||
|
if (!categoryExists) return next(createHttpError(404, "Category not found"));
|
||||||
|
|
||||||
|
// Simpan produk
|
||||||
|
const product = await productsRepository.createProduct(data);
|
||||||
|
|
||||||
|
res.status(201).json({ message: "Product created", product });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// PATCH: Perbarui produk
|
||||||
|
const updateProduct = [
|
||||||
|
param("id").isInt().withMessage("Invalid product ID"), // Ganti isMongoId() ke isInt()
|
||||||
|
body("product_name").optional().isString(),
|
||||||
|
body("description").optional().isString(),
|
||||||
|
body("price").optional().isNumeric().withMessage("Price must be a number"),
|
||||||
|
body("product_category_id").optional().isInt().withMessage("Invalid category ID"), // Ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
// Jika ada perubahan kategori, cek apakah kategori tersebut ada
|
||||||
|
if (data.product_category_id) {
|
||||||
|
const categoryExists = await productCategoriesRepository.getProductCategoryById(data.product_category_id);
|
||||||
|
if (!categoryExists) return next(createHttpError(404, "Category not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update produk
|
||||||
|
const product = await productsRepository.updateProductById(Number(id), data);
|
||||||
|
if (!product) return next(createHttpError(404, "Product not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Product updated", product });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// DELETE: Hapus produk
|
||||||
|
const deleteProduct = [
|
||||||
|
param("id").isInt().withMessage("Invalid product ID"), // Ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const product = await productsRepository.deleteProductById(Number(id));
|
||||||
|
if (!product) return next(createHttpError(404, "Product not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Product deleted" });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// GET: Ambil satu produk berdasarkan ID
|
||||||
|
const getProduct = [
|
||||||
|
param("id").isInt().withMessage("Invalid product ID"), // Ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const product = await productsRepository.getProductById(Number(id));
|
||||||
|
|
||||||
|
if (!product) return next(createHttpError(404, "Product not found"));
|
||||||
|
|
||||||
|
// Ambil kategori produk terkait (join dengan kategori produk)
|
||||||
|
const productCategory = await productCategoriesRepository.getProductCategoryById(product.product_category_id);
|
||||||
|
product.product_category = productCategory; // Menambahkan informasi kategori ke dalam produk
|
||||||
|
|
||||||
|
res.json(product);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// GET: Ambil semua produk
|
||||||
|
const getProducts = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const allProducts = await productsRepository.getAllProducts();
|
||||||
|
|
||||||
|
// Ambil kategori produk terkait untuk setiap produk
|
||||||
|
for (const product of allProducts) {
|
||||||
|
const category = await productCategoriesRepository.getProductCategoryById(product.product_category_id);
|
||||||
|
product.product_category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(allProducts);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createProduct,
|
||||||
|
updateProduct,
|
||||||
|
deleteProduct,
|
||||||
|
getProduct,
|
||||||
|
getProducts
|
||||||
|
};
|
|
@ -0,0 +1,72 @@
|
||||||
|
import express, { NextFunction, Request, Response } from "express";
|
||||||
|
import { body, param, query, matchedData } from "express-validator";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import validate from "../../middleware/expressValidatorErrorHandler";
|
||||||
|
import { predictions, products } from "../../database/models";
|
||||||
|
|
||||||
|
// 🟢 **POST: Tambah Prediksi Stok Baru**
|
||||||
|
const createStockForecasting = [
|
||||||
|
body("stock_sold").notEmpty().isNumeric().withMessage("Stock sold must be a number"),
|
||||||
|
body("stock_predicted").notEmpty().isNumeric().withMessage("Stock predicted must be a number"),
|
||||||
|
body("type").notEmpty().isString().withMessage("Type is required"),
|
||||||
|
body("accuracy").optional().isNumeric().withMessage("Accuracy must be a number"),
|
||||||
|
body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"),
|
||||||
|
body("product_id").notEmpty().isMongoId().withMessage("Invalid product ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
// Cek apakah produk ada
|
||||||
|
const productExists = await products.findById(data.product_id);
|
||||||
|
if (!productExists) return next(createHttpError(404, "Product not found"));
|
||||||
|
|
||||||
|
// Simpan data prediksi
|
||||||
|
const prediction = new predictions(data);
|
||||||
|
await prediction.save();
|
||||||
|
res.status(201).json({ message: "Stock forecasting created", prediction });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔵 **GET: Ambil Prediksi Berdasarkan Tanggal**
|
||||||
|
const getStockForecastingByDate = [
|
||||||
|
query("date").notEmpty().isISO8601().withMessage("Invalid date format"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { date } = matchedData(req);
|
||||||
|
const startDate = new Date(date);
|
||||||
|
const endDate = new Date(date);
|
||||||
|
endDate.setDate(endDate.getDate() + 1);
|
||||||
|
|
||||||
|
const forecasts = await predictions.find({
|
||||||
|
createdAt: { $gte: startDate, $lt: endDate }
|
||||||
|
}).populate("product_id user_id");
|
||||||
|
|
||||||
|
if (forecasts.length === 0) return next(createHttpError(404, "No stock forecasting found for this date"));
|
||||||
|
|
||||||
|
res.json(forecasts);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟠 **GET: Ambil Semua Prediksi (History)**
|
||||||
|
const getStockForecastings = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const forecasts = await predictions.find().populate("product_id user_id");
|
||||||
|
res.json(forecasts);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createStockForecasting,
|
||||||
|
getStockForecastingByDate,
|
||||||
|
getStockForecastings
|
||||||
|
};
|
|
@ -0,0 +1,115 @@
|
||||||
|
import express, { NextFunction, Request, Response } from "express";
|
||||||
|
import { body, param, matchedData } from "express-validator";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { purchases, products, suppliers } from "../../database/models";
|
||||||
|
import validate from "../../middleware/expressValidatorErrorHandler";
|
||||||
|
|
||||||
|
// 🟢 **POST: Tambah Pembelian Stok Baru**
|
||||||
|
const createStockPurchase = [
|
||||||
|
body("amount").notEmpty().isNumeric().withMessage("Amount must be a number"),
|
||||||
|
body("total_price").notEmpty().isNumeric().withMessage("Total price must be a number"),
|
||||||
|
body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"),
|
||||||
|
body("product_id").notEmpty().isMongoId().withMessage("Invalid product ID"),
|
||||||
|
body("supplier_id").notEmpty().isMongoId().withMessage("Invalid supplier ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
// Cek apakah produk ada
|
||||||
|
const productExists = await products.findById(data.product_id);
|
||||||
|
if (!productExists) return next(createHttpError(404, "Product not found"));
|
||||||
|
|
||||||
|
// Cek apakah supplier ada
|
||||||
|
const supplierExists = await suppliers.findById(data.supplier_id);
|
||||||
|
if (!supplierExists) return next(createHttpError(404, "Supplier not found"));
|
||||||
|
|
||||||
|
// Simpan data pembelian
|
||||||
|
const purchase = new purchases(data);
|
||||||
|
await purchase.save();
|
||||||
|
res.status(201).json({ message: "Stock purchase created", purchase });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟠 **PATCH: Perbarui Pembelian Stok**
|
||||||
|
const updateStockPurchase = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid purchase ID"),
|
||||||
|
body("amount").optional().isNumeric().withMessage("Amount must be a number"),
|
||||||
|
body("total_price").optional().isNumeric().withMessage("Total price must be a number"),
|
||||||
|
body("supplier_id").optional().isMongoId().withMessage("Invalid supplier ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
if (data.supplier_id) {
|
||||||
|
const supplierExists = await suppliers.findById(data.supplier_id);
|
||||||
|
if (!supplierExists) return next(createHttpError(404, "Supplier not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const purchase = await purchases.findByIdAndUpdate(id, data, { new: true });
|
||||||
|
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Stock purchase updated", purchase });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔴 **DELETE: Hapus Pembelian Stok**
|
||||||
|
const deleteStockPurchase = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid purchase ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const purchase = await purchases.findByIdAndDelete(id);
|
||||||
|
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Stock purchase deleted" });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔵 **GET: Ambil Satu Pembelian Stok Berdasarkan ID**
|
||||||
|
const getStockPurchase = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid purchase ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const purchase = await purchases.findById(id).populate("product_id supplier_id user_id");
|
||||||
|
|
||||||
|
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
|
||||||
|
|
||||||
|
res.json(purchase);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟡 **GET: Ambil Semua Pembelian Stok (Histori)**
|
||||||
|
const getStockPurchases = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const allPurchases = await purchases.find().populate("product_id supplier_id user_id");
|
||||||
|
res.json(allPurchases);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createStockPurchase,
|
||||||
|
updateStockPurchase,
|
||||||
|
deleteStockPurchase,
|
||||||
|
getStockPurchase,
|
||||||
|
getStockPurchases
|
||||||
|
};
|
|
@ -0,0 +1,101 @@
|
||||||
|
import express, { NextFunction, Request, Response } from "express";
|
||||||
|
import { body, param, matchedData } from "express-validator";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { suppliers } from "../../database/models";
|
||||||
|
import validate from "../../middleware/expressValidatorErrorHandler";
|
||||||
|
|
||||||
|
// 🟢 **POST: Tambah Supplier Baru**
|
||||||
|
const createSupplier = [
|
||||||
|
body("supplier_name").notEmpty().withMessage("Supplier name is required"),
|
||||||
|
body("contact").optional().isString().withMessage("Contact must be a string"),
|
||||||
|
body("address").optional().isString().withMessage("Address must be a string"),
|
||||||
|
body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
const supplier = new suppliers(data);
|
||||||
|
await supplier.save();
|
||||||
|
|
||||||
|
res.status(201).json({ message: "Supplier created", supplier });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟠 **PATCH: Perbarui Supplier**
|
||||||
|
const updateSupplier = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid supplier ID"),
|
||||||
|
body("supplier_name").optional().isString(),
|
||||||
|
body("contact").optional().isString(),
|
||||||
|
body("address").optional().isString(),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data = matchedData(req);
|
||||||
|
|
||||||
|
const supplier = await suppliers.findByIdAndUpdate(id, data, { new: true });
|
||||||
|
if (!supplier) return next(createHttpError(404, "Supplier not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Supplier updated", supplier });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔴 **DELETE: Hapus Supplier**
|
||||||
|
const deleteSupplier = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid supplier ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const supplier = await suppliers.findByIdAndDelete(id);
|
||||||
|
if (!supplier) return next(createHttpError(404, "Supplier not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Supplier deleted" });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔵 **GET: Ambil Satu Supplier Berdasarkan ID**
|
||||||
|
const getSupplier = [
|
||||||
|
param("id").isMongoId().withMessage("Invalid supplier ID"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const supplier = await suppliers.findById(id).populate("user_id");
|
||||||
|
|
||||||
|
if (!supplier) return next(createHttpError(404, "Supplier not found"));
|
||||||
|
|
||||||
|
res.json(supplier);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟡 **GET: Ambil Semua Supplier**
|
||||||
|
const getSuppliers = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const allSuppliers = await suppliers.find().populate("user_id");
|
||||||
|
res.json(allSuppliers);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createSupplier,
|
||||||
|
updateSupplier,
|
||||||
|
deleteSupplier,
|
||||||
|
getSupplier,
|
||||||
|
getSuppliers
|
||||||
|
};
|
|
@ -0,0 +1,108 @@
|
||||||
|
// controllers/transactionsController.ts
|
||||||
|
|
||||||
|
import { body, param, matchedData } from "express-validator";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
|
||||||
|
import * as transactionsRepository from "../../repository/transaction"; // Repository untuk Transactions dalam bentuk mongoose
|
||||||
|
import * as productsRepository from "../../repository/products"; // Untuk cek produk
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { ITransaction } from "../../types/db-model";
|
||||||
|
|
||||||
|
// 🟢 **POST: Tambah Transaksi Baru**
|
||||||
|
const createTransaction = [
|
||||||
|
body("amount").notEmpty().isNumeric().withMessage("Amount must be a number"),
|
||||||
|
body("total_price").notEmpty().isNumeric().withMessage("Total price must be a number"),
|
||||||
|
body("user_id").notEmpty().isInt().withMessage("Invalid user ID"), // isMongoId() diganti isInt()
|
||||||
|
body("product_id").notEmpty().isInt().withMessage("Invalid product ID"), // isMongoId() diganti isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData<ITransaction>(req);
|
||||||
|
|
||||||
|
// Cek apakah produk ada
|
||||||
|
const productExists = await productsRepository.getProductById(data.product_id);
|
||||||
|
if (!productExists) return next(createHttpError(404, "Product not found"));
|
||||||
|
|
||||||
|
// Simpan data transaksi
|
||||||
|
const transaction = await transactionsRepository.createTransaction(data);
|
||||||
|
|
||||||
|
res.status(201).json({ message: "Transaction created", transaction });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟠 **PATCH: Perbarui Transaksi**
|
||||||
|
const updateTransaction = [
|
||||||
|
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
|
||||||
|
body("amount").optional().isNumeric().withMessage("Amount must be a number"),
|
||||||
|
body("total_price").optional().isNumeric().withMessage("Total price must be a number"),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data = matchedData<ITransaction>(req);
|
||||||
|
|
||||||
|
const transaction = await transactionsRepository.updateTransactionById(Number(id), data);
|
||||||
|
if (!transaction) return next(createHttpError(404, "Transaction not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Transaction updated", transaction });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔴 **DELETE: Hapus Transaksi**
|
||||||
|
const deleteTransaction = [
|
||||||
|
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const transaction = await transactionsRepository.deleteTransactionById(Number(id));
|
||||||
|
if (!transaction) return next(createHttpError(404, "Transaction not found"));
|
||||||
|
|
||||||
|
res.json({ message: "Transaction deleted" });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔵 **GET: Ambil Satu Transaksi Berdasarkan ID**
|
||||||
|
const getTransaction = [
|
||||||
|
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const transaction = await transactionsRepository.getTransactionById(Number(id));
|
||||||
|
|
||||||
|
if (!transaction) return next(createHttpError(404, "Transaction not found"));
|
||||||
|
|
||||||
|
res.json(transaction);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟡 **GET: Ambil Semua Transaksi**
|
||||||
|
const getTransactions = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const allTransactions = await transactionsRepository.getAllTransactions();
|
||||||
|
res.json(allTransactions);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createTransaction,
|
||||||
|
updateTransaction,
|
||||||
|
deleteTransaction,
|
||||||
|
getTransaction,
|
||||||
|
getTransactions
|
||||||
|
};
|
|
@ -0,0 +1,103 @@
|
||||||
|
// controllers/userFilesController.ts
|
||||||
|
|
||||||
|
import { body, param, matchedData } from "express-validator";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
|
||||||
|
import * as userFilesRepository from "../../repository/user-files"; // Repository untuk User Files
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { IUserFile } from "../../types/db-model";
|
||||||
|
|
||||||
|
// 🟢 **POST: Tambah User File Baru**
|
||||||
|
const createUserFile = [
|
||||||
|
body("file_name").notEmpty().withMessage("File name is required"),
|
||||||
|
body("saved_file_name").notEmpty().withMessage("Saved file name is required"),
|
||||||
|
body("is_converted").optional().isBoolean().withMessage("is_converted must be a boolean"),
|
||||||
|
body("user_id").notEmpty().isInt().withMessage("Invalid user ID"), // ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const data = matchedData<IUserFile>(req);
|
||||||
|
const userFile = await userFilesRepository.createUserFile(data);
|
||||||
|
|
||||||
|
res.status(201).json({ message: "User file created", userFile });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟠 **PATCH: Perbarui File User**
|
||||||
|
const updateUserFile = [
|
||||||
|
param("id").isInt().withMessage("Invalid file ID"), // ganti isMongoId() ke isInt()
|
||||||
|
body("file_name").optional().isString(),
|
||||||
|
body("saved_file_name").optional().isString(),
|
||||||
|
body("is_converted").optional().isBoolean(),
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const data = matchedData<IUserFile>(req);
|
||||||
|
|
||||||
|
const userFile = await userFilesRepository.updateUserFileById(Number(id), data);
|
||||||
|
if (!userFile) return next(createHttpError(404, "User file not found"));
|
||||||
|
|
||||||
|
res.json({ message: "User file updated", userFile });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔴 **DELETE: Hapus File User**
|
||||||
|
const deleteUserFile = [
|
||||||
|
param("id").isInt().withMessage("Invalid file ID"), // ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const userFile = await userFilesRepository.deleteUserFileById(Number(id));
|
||||||
|
if (!userFile) return next(createHttpError(404, "User file not found"));
|
||||||
|
|
||||||
|
res.json({ message: "User file deleted" });
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🔵 **GET: Ambil Satu File User Berdasarkan ID**
|
||||||
|
const getUserFile = [
|
||||||
|
param("id").isInt().withMessage("Invalid file ID"), // ganti isMongoId() ke isInt()
|
||||||
|
validate,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const userFile = await userFilesRepository.getUserFileById(Number(id));
|
||||||
|
|
||||||
|
if (!userFile) return next(createHttpError(404, "User file not found"));
|
||||||
|
|
||||||
|
res.json(userFile);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 🟡 **GET: Ambil Semua File User**
|
||||||
|
const getUserFiles = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const allUserFiles = await userFilesRepository.getAllUserFiles();
|
||||||
|
res.json(allUserFiles);
|
||||||
|
} catch (err) {
|
||||||
|
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
createUserFile,
|
||||||
|
updateUserFile,
|
||||||
|
deleteUserFile,
|
||||||
|
getUserFile,
|
||||||
|
getUserFiles
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
import knex from 'knex';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const {
|
||||||
|
DB_USERNAME,
|
||||||
|
DB_PASSWORD,
|
||||||
|
DB_DATABASE,
|
||||||
|
DB_HOST = 'localhost',
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
if (!DB_USERNAME || !DB_DATABASE) {
|
||||||
|
throw new Error("Missing required environment variables: DB_USERNAME, DB_PASSWORD, DB_DATABASE");
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = knex({
|
||||||
|
client: 'mysql2',
|
||||||
|
connection: {
|
||||||
|
host: DB_HOST,
|
||||||
|
user: DB_USERNAME,
|
||||||
|
password: DB_PASSWORD,
|
||||||
|
database: DB_DATABASE,
|
||||||
|
},
|
||||||
|
pool: { min: 0, max: 10 },
|
||||||
|
});
|
||||||
|
|
||||||
|
async function connectDB() {
|
||||||
|
try {
|
||||||
|
await db.raw('SELECT 1');
|
||||||
|
console.log('✅ Connected to MySQL using Knex');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ MySQL Connection Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { db };
|
||||||
|
export default connectDB;
|
|
@ -0,0 +1,97 @@
|
||||||
|
// database/migrations/2025XXXX_create_all_tables.js
|
||||||
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex) {
|
||||||
|
// Users
|
||||||
|
await knex.schema.createTable('users', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('email').notNullable().unique();
|
||||||
|
table.string('username').unique();
|
||||||
|
table.string('password').notNullable();
|
||||||
|
table.string('name');
|
||||||
|
table.string('profile_picture');
|
||||||
|
table.boolean('is_verified').defaultTo(false);
|
||||||
|
table.string('refresh_token').defaultTo('');
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Product Categories
|
||||||
|
await knex.schema.createTable('product_categories', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('category_name').notNullable();
|
||||||
|
table.string('description');
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Products
|
||||||
|
await knex.schema.createTable('products', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('product_name').notNullable();
|
||||||
|
table.integer('stock').notNullable();
|
||||||
|
table.float('price').notNullable();
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
table.integer('product_category_id').unsigned().references('id').inTable('product_categories').onDelete('SET NULL');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suppliers
|
||||||
|
await knex.schema.createTable('suppliers', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('supplier_name').notNullable();
|
||||||
|
table.string('contact');
|
||||||
|
table.string('address');
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Purchases
|
||||||
|
await knex.schema.createTable('purchases', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.date('buying_date').defaultTo(knex.fn.now()).notNullable();
|
||||||
|
table.integer('amount').notNullable();
|
||||||
|
table.float('total_price').notNullable();
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
table.integer('product_id').unsigned().references('id').inTable('products').onDelete('CASCADE');
|
||||||
|
table.integer('supplier_id').unsigned().references('id').inTable('suppliers').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
await knex.schema.createTable('transactions', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.date('transaction_date').defaultTo(knex.fn.now()).notNullable();
|
||||||
|
table.integer('amount').notNullable();
|
||||||
|
table.float('total_price').notNullable();
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
table.integer('product_id').unsigned().references('id').inTable('products').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Predictions
|
||||||
|
await knex.schema.createTable('predictions', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('stock_sold').notNullable();
|
||||||
|
table.integer('stock_predicted').notNullable();
|
||||||
|
table.string('type').notNullable();
|
||||||
|
table.float('accuracy');
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
table.integer('product_id').unsigned().references('id').inTable('products').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
|
||||||
|
// User Files
|
||||||
|
await knex.schema.createTable('user_files', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('file_name').notNullable();
|
||||||
|
table.string('saved_file_name').notNullable().unique();
|
||||||
|
table.boolean('is_converted').defaultTo(false);
|
||||||
|
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex) {
|
||||||
|
await knex.schema
|
||||||
|
.dropTableIfExists('user_files')
|
||||||
|
.dropTableIfExists('predictions')
|
||||||
|
.dropTableIfExists('transactions')
|
||||||
|
.dropTableIfExists('purchases')
|
||||||
|
.dropTableIfExists('suppliers')
|
||||||
|
.dropTableIfExists('products')
|
||||||
|
.dropTableIfExists('product_categories')
|
||||||
|
.dropTableIfExists('users');
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { RequestHandler } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { TAccessToken } from "../types/jwt";
|
||||||
|
|
||||||
|
const authenticate: RequestHandler = (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return next(createHttpError(401, "Access token required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
||||||
|
if (err) return next(createHttpError(403, "Invalid or expired access token"));
|
||||||
|
|
||||||
|
req.user = decoded as TAccessToken;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authenticate;
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ErrorRequestHandler } from "express";
|
||||||
|
import { TAPIResponse } from "../../types/core/http";
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export const respondWithError: ErrorRequestHandler = (err, req, res, _next) => {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.xhr ||
|
||||||
|
req.headers["content-type"] === "application/json" ||
|
||||||
|
req.headers.accept?.includes("application/json")
|
||||||
|
) {
|
||||||
|
const errorResponse: TAPIResponse<any[]> = { success: false }
|
||||||
|
|
||||||
|
if ([
|
||||||
|
'Access token required',
|
||||||
|
'Invalid or expired access token',
|
||||||
|
'No refresh token provided',
|
||||||
|
'Invalid refresh token'
|
||||||
|
].includes(err.message)) {
|
||||||
|
errorResponse.message = err.message
|
||||||
|
} else if (err.message === 'Validation Error') {
|
||||||
|
errorResponse.message = err.message
|
||||||
|
errorResponse.error = err.cause
|
||||||
|
} else {
|
||||||
|
errorResponse.message = err.message || "Internal Server Error"
|
||||||
|
errorResponse.error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(errorResponse);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.message = `${err.message}`;
|
||||||
|
res.locals.error = process.env.NODE_ENV === 'development' ? err : {};
|
||||||
|
res.render('error');
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
import createHttpError from 'http-errors';
|
||||||
|
|
||||||
|
const expressValidatorErrorHandler: RequestHandler = (req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return next(createHttpError(400, "Validation Error", { cause: errors.array() }));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default expressValidatorErrorHandler
|
|
@ -0,0 +1,8 @@
|
||||||
|
body {
|
||||||
|
padding: 50px;
|
||||||
|
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00B7FF;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// repository/productsRepository.ts
|
||||||
|
|
||||||
|
import { db } from "../database/MySQL"; // Koneksi database menggunakan Knex
|
||||||
|
import { IProduct } from "../types/db-model";
|
||||||
|
|
||||||
|
const TABLE_NAME = "products";
|
||||||
|
|
||||||
|
// Tambah produk baru
|
||||||
|
export const createProduct = async (data: IProduct) => {
|
||||||
|
try {
|
||||||
|
const [product] = await db(TABLE_NAME).insert({
|
||||||
|
product_name: data.product_name,
|
||||||
|
price: data.price,
|
||||||
|
product_category_id: data.product_category_id,
|
||||||
|
}).returning("*");
|
||||||
|
|
||||||
|
return product;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Error creating product");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update produk by ID
|
||||||
|
export const updateProductById = async (id: number, data: Partial<IProduct>) => {
|
||||||
|
try {
|
||||||
|
const [updatedProduct] = await db(TABLE_NAME)
|
||||||
|
.where({ id })
|
||||||
|
.update(data)
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
return updatedProduct;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Error updating product");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hapus produk by ID
|
||||||
|
export const deleteProductById = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const deleted = await db(TABLE_NAME).where({ id }).del();
|
||||||
|
return deleted;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Error deleting product");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ambil produk by ID
|
||||||
|
export const getProductById = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const product = await db(TABLE_NAME).where({ id }).first();
|
||||||
|
return product;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Error fetching product");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ambil semua produk
|
||||||
|
export const getAllProducts = async () => {
|
||||||
|
try {
|
||||||
|
return await db(TABLE_NAME);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("Error fetching all products");
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
// repository/transactionsRepository.ts
|
||||||
|
|
||||||
|
import { db } from '../database/MySQL';
|
||||||
|
import { ITransaction } from '../types/db-model';
|
||||||
|
|
||||||
|
export const createTransaction = async (data: ITransaction) => {
|
||||||
|
try {
|
||||||
|
const [transaction] = await db('transactions').insert(data).returning('*');
|
||||||
|
return transaction;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error creating transaction');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTransactionById = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const transaction = await db('transactions')
|
||||||
|
.where({ id })
|
||||||
|
.join('products', 'transactions.product_id', '=', 'products.id')
|
||||||
|
.join('users', 'transactions.user_id', '=', 'users.id')
|
||||||
|
.select('transactions.*', 'products.product_name', 'users.username');
|
||||||
|
return transaction[0];
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error fetching transaction');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTransactionById = async (id: number, data: ITransaction) => {
|
||||||
|
try {
|
||||||
|
const [updatedTransaction] = await db('transactions').where({ id }).update(data).returning('*');
|
||||||
|
return updatedTransaction;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error updating transaction');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTransactionById = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const transaction = await db('transactions').where({ id }).del();
|
||||||
|
return transaction;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error deleting transaction');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllTransactions = async () => {
|
||||||
|
try {
|
||||||
|
return await db('transactions')
|
||||||
|
.join('products', 'transactions.product_id', '=', 'products.id')
|
||||||
|
.join('users', 'transactions.user_id', '=', 'users.id')
|
||||||
|
.select('transactions.*', 'products.product_name', 'users.username');
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error fetching all transactions');
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,48 @@
|
||||||
|
// repository/userFilesRepository.ts
|
||||||
|
|
||||||
|
import { db } from '../database/MySQL';
|
||||||
|
import { IUserFile } from '../types/db-model';
|
||||||
|
|
||||||
|
export const createUserFile = async (data: IUserFile) => {
|
||||||
|
try {
|
||||||
|
const [userFile] = await db('user_files').insert(data).returning('*');
|
||||||
|
return userFile;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error creating user file');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserFileById = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const userFile = await db('user_files').where({ id }).first();
|
||||||
|
return userFile;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error fetching user file');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserFileById = async (id: number, data: IUserFile) => {
|
||||||
|
try {
|
||||||
|
const [updatedUserFile] = await db('user_files').where({ id }).update(data).returning('*');
|
||||||
|
return updatedUserFile;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error updating user file');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUserFileById = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const userFile = await db('user_files').where({ id }).del();
|
||||||
|
return userFile;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error deleting user file');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllUserFiles = async () => {
|
||||||
|
try {
|
||||||
|
return await db('user_files');
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error fetching all user files');
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { db } from "../database/MySQL"
|
||||||
|
import { IUserTable } from "../types/db-model"
|
||||||
|
import { TCreateUserRequest } from "../types/request/users-req";
|
||||||
|
|
||||||
|
export const findUserById = (id: number) => {
|
||||||
|
return db<IUserTable>('users').where({ id });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findUserByEmail = (email: string) => {
|
||||||
|
return db<IUserTable>('users').where({ email })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findUserByRefreshToken = (refresh_token: string) => {
|
||||||
|
return db<IUserTable>('users').where({ refresh_token });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUserByCondition = (condition: Partial<IUserTable>, data: Partial<IUserTable>) => {
|
||||||
|
return db<IUserTable>('users').where(condition).update(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUser = async ({ email, name, password }: TCreateUserRequest) => {
|
||||||
|
const [id] = await db<IUserTable>('users').insert({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
return id
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import express from 'express'
|
||||||
|
var router = express.Router();
|
||||||
|
import authRoute from '../../controller/api/authController';
|
||||||
|
|
||||||
|
router.post('/register', authRoute.register);
|
||||||
|
router.post('/login', authRoute.login);
|
||||||
|
router.post('/forgot-password', authRoute.forgotPasswordSendEmail);
|
||||||
|
router.get('/forgot-password/:token', authRoute.forgotPasswordVerifyToken);
|
||||||
|
router.patch('/forgot-password/:token', authRoute.forgotPasswordChangePassword);
|
||||||
|
router.get('/verify/:token', authRoute.verify);
|
||||||
|
router.post('/re-send-email-activation/:token', authRoute.resendEmailVerification);
|
||||||
|
router.get('/refresh-token', authRoute.refreshToken);
|
||||||
|
router.get('/logout', authRoute.logout);
|
||||||
|
|
||||||
|
export default router
|
|
@ -0,0 +1,80 @@
|
||||||
|
import express from 'express'
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
import authEndpoint from './auth'
|
||||||
|
import authenticate from '../../middleware/authMiddleware';
|
||||||
|
// import productCategory from '../../controller/api/productCategoriesController';
|
||||||
|
// import product from '../../controller/api/productsController';
|
||||||
|
// import stockForecastingController from '../../controller/api/stockForecasting';
|
||||||
|
// import stockPurchaseController from '../../controller/api/stockPurchases';
|
||||||
|
// import supplierController from '../../controller/api/suppliers';
|
||||||
|
// import transactionController from '../../controller/api/transactions';
|
||||||
|
// import userFileController from '../../controller/api/userFiles';
|
||||||
|
// const {jwtInfluencerMiddleware} = require("../middleware/authMiddleware");
|
||||||
|
|
||||||
|
router.get('/', function (req, res, next) {
|
||||||
|
res.send('Read docs.');
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use('/auth', authEndpoint)
|
||||||
|
|
||||||
|
router.use('/is-authenticated', authenticate)
|
||||||
|
router.get('/is-authenticated', (req, res) => {
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----- Product Categories
|
||||||
|
// router.post("/product-category", productCategory.createCategory);
|
||||||
|
// router.get("/product-categories", productCategory.getCategories);
|
||||||
|
// router.get("/product-category/:id", productCategory.getCategory);
|
||||||
|
// router.patch("/product-category/:id", productCategory.updateCategory);
|
||||||
|
// router.delete("/product-category/:id", productCategory.deleteCategory);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Products
|
||||||
|
// router.get("/product/:id", product.getProduct);
|
||||||
|
// router.post("/product", product.createProduct);
|
||||||
|
// router.patch("/product/:id", product.updateProduct);
|
||||||
|
// router.delete("/product/:id", product.deleteProduct);
|
||||||
|
// router.get("/products", product.getProducts);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Stock Forecasting
|
||||||
|
// router.post("/stock-forecasting", stockForecastingController.createStockForecasting);
|
||||||
|
// router.get("/stock-forecasting", stockForecastingController.getStockForecastingByDate);
|
||||||
|
// router.get("/stock-forecasting/history", stockForecastingController.getStockForecastings);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Stock Purchases
|
||||||
|
// router.post("/stock-purchase", stockPurchaseController.createStockPurchase);
|
||||||
|
// router.patch("/stock-purchase/:id", stockPurchaseController.updateStockPurchase);
|
||||||
|
// router.delete("/stock-purchase/:id", stockPurchaseController.deleteStockPurchase);
|
||||||
|
// router.get("/stock-purchase/:id", stockPurchaseController.getStockPurchase);
|
||||||
|
// router.get("/stock-purchase", stockPurchaseController.getStockPurchases);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Suppliers
|
||||||
|
// router.post("/supplier", supplierController.createSupplier);
|
||||||
|
// router.patch("/supplier/:id", supplierController.updateSupplier);
|
||||||
|
// router.delete("/supplier/:id", supplierController.deleteSupplier);
|
||||||
|
// router.get("/supplier/:id", supplierController.getSupplier);
|
||||||
|
// router.get("/supplier", supplierController.getSuppliers);
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Transactions
|
||||||
|
// router.post("/transaction", transactionController.createTransaction);
|
||||||
|
// router.patch("/transaction/:id", transactionController.updateTransaction);
|
||||||
|
// router.delete("/transaction/:id", transactionController.deleteTransaction);
|
||||||
|
// router.get("/transaction/:id", transactionController.getTransaction);
|
||||||
|
// router.get("/transaction", transactionController.getTransactions);
|
||||||
|
|
||||||
|
// ----- userFile
|
||||||
|
// router.post("/user-files", userFileController.createUserFile);
|
||||||
|
// router.patch("/user-files/:id", userFileController.updateUserFile);
|
||||||
|
// router.delete("/user-files/:id", userFileController.deleteUserFile);
|
||||||
|
// router.get("/user-files/:id", userFileController.getUserFile);
|
||||||
|
// router.get("/user-files", userFileController.getUserFiles);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -0,0 +1,11 @@
|
||||||
|
// src/server.ts
|
||||||
|
import app from './app';
|
||||||
|
import http from 'http';
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running at http://localhost:${PORT}`);
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { findUserByEmail, findUserById, findUserByRefreshToken, updateUserByCondition } from "../repository/usersRepository";
|
||||||
|
import { IUserTable } from "../types/db-model";
|
||||||
|
|
||||||
|
export const getUserById = (id: number) => findUserById(id).first()
|
||||||
|
export const getUserByEmail = (email: string) => findUserByEmail(email).first()
|
||||||
|
export const getUserByRefreshToken = (refresh_token: string) =>
|
||||||
|
findUserByRefreshToken(refresh_token).first()
|
||||||
|
export const activateUser = (email: string) => findUserByEmail(email).update({ is_verified: true })
|
||||||
|
export const updateUserById = (id: number, data: Partial<IUserTable>) => updateUserByCondition({ id }, data)
|
|
@ -0,0 +1,25 @@
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
NODE_ENV: 'development' | 'production' | 'test';
|
||||||
|
HOST: string;
|
||||||
|
PORT: string;
|
||||||
|
ALLOWED_ORIGIN: string;
|
||||||
|
|
||||||
|
DB_CONNECTION: 'mysql' | string;
|
||||||
|
DB_HOST: string;
|
||||||
|
DB_PORT: string;
|
||||||
|
DB_DATABASE: string;
|
||||||
|
DB_USERNAME: string;
|
||||||
|
DB_PASSWORD: string;
|
||||||
|
|
||||||
|
ATLAS_USERNAME: string;
|
||||||
|
ATLAS_PASSWORD: string;
|
||||||
|
ATLAS_DBNAME: string;
|
||||||
|
|
||||||
|
JWT_SECRET: string;
|
||||||
|
JWT_REFRESH_SECRET: string;
|
||||||
|
|
||||||
|
EMAIL_USER: string;
|
||||||
|
EMAIL_PASS: string;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { IUser } from "../../db-model";
|
||||||
|
import { TAccessToken } from "../../jwt";
|
||||||
|
import { TAPIResponse, TPaginatedResponse } from "../http";
|
||||||
|
|
||||||
|
declare module "express-serve-static-core" {
|
||||||
|
interface Request {
|
||||||
|
user?: TAccessToken;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type TAPIResponse<T = Record<string, any>> = {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: Record<string, any>,
|
||||||
|
data?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPaginatedResponse<T = Record<string, any>> = TAPIResponse<T> & {
|
||||||
|
pagination?: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,71 @@
|
||||||
|
export interface IUserTable {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
is_verified: boolean;
|
||||||
|
refresh_token: string | null;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProductCategoryTable {
|
||||||
|
user_id: number;
|
||||||
|
category_name: string;
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProductTable {
|
||||||
|
id: number;
|
||||||
|
product_name: string;
|
||||||
|
stock: number;
|
||||||
|
price: number;
|
||||||
|
user_id: number;
|
||||||
|
product_category_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISupplierTable {
|
||||||
|
id: number;
|
||||||
|
supplier_name: string;
|
||||||
|
contact: string;
|
||||||
|
address: string;
|
||||||
|
user_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchaseTable {
|
||||||
|
id: number;
|
||||||
|
buying_date: Date;
|
||||||
|
amount: number;
|
||||||
|
total_price: number;
|
||||||
|
user_id: number;
|
||||||
|
product_id: number;
|
||||||
|
supplier_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITransactionTable {
|
||||||
|
id: number;
|
||||||
|
transaction_date: Date;
|
||||||
|
amount: number;
|
||||||
|
total_price: number;
|
||||||
|
user_id: number;
|
||||||
|
product_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPredictionTable {
|
||||||
|
id: number;
|
||||||
|
stock_sold: number;
|
||||||
|
stock_predicted: number;
|
||||||
|
type: string;
|
||||||
|
accuracy: number;
|
||||||
|
user_id: number;
|
||||||
|
product_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserFileTable {
|
||||||
|
id: number;
|
||||||
|
file_name: string;
|
||||||
|
saved_file_name: string;
|
||||||
|
is_converted: boolean;
|
||||||
|
user_id: number;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export type TRefreshToken = {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TAccessToken = TRefreshToken & {
|
||||||
|
email: string;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type TCreateUserRequest = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
password_confirmation?: string
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { TAPIResponse } from "../core/http";
|
||||||
|
import { IUserTable } from "../db-model";
|
||||||
|
|
||||||
|
export type TUserDetailResponse = TAPIResponse<IUserTable>
|
|
@ -0,0 +1,26 @@
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { TAccessToken, TRefreshToken } from '../../types/jwt';
|
||||||
|
import { IUserTable } from '../../types/db-model';
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const generateAccessToken = (user: IUserTable) => {
|
||||||
|
return jwt.sign(
|
||||||
|
{ id: user.id, email: user.email } as TAccessToken,
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{ expiresIn: "15m" }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRefreshToken = (user: IUserTable) => {
|
||||||
|
return jwt.sign(
|
||||||
|
{ id: user.id } as TRefreshToken,
|
||||||
|
process.env.JWT_REFRESH_SECRET,
|
||||||
|
{ expiresIn: "30d" }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
generateAccessToken,
|
||||||
|
generateRefreshToken
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config({ path: '.env' })
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export function root_path(pathStr: string) {
|
||||||
|
return path.resolve(process.cwd(), pathStr);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<h1>
|
||||||
|
<%= error.status || 500 %>
|
||||||
|
</h1>
|
||||||
|
<h2>
|
||||||
|
<%= message %>
|
||||||
|
</h2>
|
||||||
|
<pre><%= error.cause ? JSON.stringify(error.cause) : 'Unknown cause' %></pre>
|
||||||
|
<pre><%= error.stack ? error.stack : 'No stack trace available' %></pre>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= title %></title>
|
||||||
|
<link rel='stylesheet' href='/stylesheets/style.css' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><%= title %></h1>
|
||||||
|
<p>Welcome to <%= title %></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,113 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "libReplacement": true, /* Enable lib replacement. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": ["./types", "../node_modules/@types"], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "./bin/build", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||||
|
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue