auto commit

This commit is contained in:
bangdk 2024-11-02 02:05:37 +09:00
parent 208ba0d1cb
commit 1a7cfe9c0e
37 changed files with 10556 additions and 10342 deletions

View File

@ -24,6 +24,7 @@ wacefems/
│ │ │ ├── support/ # 기술 지원
│ │ │ └── settings/ # 시스템 설정
│ │ ├── components/
│ │ ├── lib/ # 라이브러리
│ │ ├── services/
│ │ └── types/
@ -90,6 +91,7 @@ wacefems/
│ │ │ ├── charts/ # 차트 컴포넌트
│ │ │ └── forms/ # 폼 컴포넌트
│ │ │
│ │ ├── lib/ # 라이브러리
│ │ ├── hooks/ # 커스텀 훅
│ │ ├── services/ # API 서비스
│ │ ├── stores/ # 상태 관리
@ -111,6 +113,8 @@ wacefems/
│ │ │ ├── monitoring/
│ │ │ └── ...
│ │ │
│ │ ├── routes/ # 라우터
│ │ ├── utils/ # 유틸리티
│ │ ├── models/ # 데이터 모델
│ │ ├── services/ # 비즈니스 로직
│ │ └── middleware/ # 미들웨어
@ -124,7 +128,21 @@ wacefems/
```
### 2.2 기술 스택
### 2.2 데이터베이스 모델
## 데이터베이스 생성
```bash
createdb wacefems_database
```
### 데이터베이스 마이그레이션
```bash
npx sequelize-cli db:migrate
```
### 2.3 기술 스택
#### Frontend

View File

@ -60,7 +60,7 @@ services:
- redis
postgres:
image: postgres:15-alpine
image: postgres:16-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]

File diff suppressed because it is too large Load Diff

2732
fems-admin/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

13
fems-api/.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"env": {
"node": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2021
},
"rules": {
"no-console": "warn"
}
}

8
fems-api/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# .gitignore
node_modules/
.env
.env.*
!.env.example
logs/
coverage/
.DS_Store

32
fems-api/package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "fems-api",
"version": "1.0.0",
"description": "Factory Energy Management System API",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"lint": "eslint src/",
"test": "jest"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.0",
"pg": "^8.11.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.31.1",
"winston": "^3.8.2"
},
"devDependencies": {
"eslint": "^8.40.0",
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3"
}
}

56
fems-api/src/app.js Normal file
View File

@ -0,0 +1,56 @@
// src/app.js
const express = require("express");
const cors = require("cors");
const config = require("./config/config");
const { sequelize } = require("./models");
const logger = require("./config/logger");
const requestLogger = require("./middleware/requestLogger.middleware");
const errorHandler = require("./middleware/errorHandler.middleware");
// Import routes
const adminRoutes = require("./routes/admin");
const appRoutes = require("./routes/app");
const app = express();
// Middleware
app.use(cors(config.cors));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(requestLogger);
// Routes
app.use("/api/v1/admin", adminRoutes);
app.use("/api/v1/app", appRoutes);
// Error handling
app.use(errorHandler);
// Database initialization and server start
const initializeServer = async () => {
try {
await sequelize.authenticate();
logger.info("Database connection established successfully.");
// Sync database (in development only)
if (process.env.NODE_ENV !== "production") {
await sequelize.sync({ alter: true });
logger.info("Database synchronized.");
}
// Create initial super admin if not exists
await require("./utils/createInitialAdmin")();
const port = config.port;
app.listen(port, () => {
logger.info(`Server is running on port ${port}`);
});
} catch (error) {
logger.error("Unable to start server:", error);
process.exit(1);
}
};
initializeServer();
module.exports = app;

View File

@ -0,0 +1,45 @@
// src/config/config.js
require("dotenv").config();
module.exports = {
development: {
port: process.env.PORT || 3001,
database: {
host: process.env.POSTGRES_HOST || "localhost",
port: parseInt(process.env.POSTGRES_PORT) || 5432,
username: process.env.POSTGRES_USER || "fems_user",
password: process.env.POSTGRES_PASSWORD || "fems_password",
database: process.env.POSTGRES_DB || "fems_dev",
dialect: "postgres",
logging: true,
},
jwt: {
secret: process.env.JWT_SECRET || "your-dev-secret-key",
expiresIn: process.env.JWT_EXPIRES_IN || "1d",
},
cors: {
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
credentials: true,
},
},
production: {
port: process.env.PORT || 3001,
database: {
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT),
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
dialect: "postgres",
logging: false,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || "1d",
},
cors: {
origin: process.env.CORS_ORIGIN,
credentials: true,
},
},
}[process.env.NODE_ENV || "development"];

View File

@ -0,0 +1,22 @@
// src/config/database.js
const { Sequelize } = require('sequelize');
const config = require('./config');
const sequelize = new Sequelize(
config.database.database,
config.database.username,
config.database.password,
{
host: config.database.host,
port: config.database.port,
dialect: config.database.dialect,
logging: config.database.logging ? console.log : false,
define: {
timestamps: true,
underscored: true
}
}
);
module.exports = sequelize;

View File

@ -0,0 +1,33 @@
// src/config/logger.js
const winston = require("winston");
const path = require("path");
const logger = winston.createLogger({
level: process.env.NODE_ENV === "production" ? "info" : "debug",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: path.join(__dirname, "../../logs/error.log"),
level: "error",
}),
new winston.transports.File({
filename: path.join(__dirname, "../../logs/combined.log"),
}),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
})
);
}
module.exports = logger;

View File

@ -0,0 +1,37 @@
// src/controllers/app/auth/auth.controller.js
const express = require("express");
const router = express.Router();
const authService = require("../../../services/auth.service");
const { body } = require("express-validator");
const validate = require("../../../middleware/validator.middleware");
const authMiddleware = require("../../../middleware/auth.middleware");
router.post(
"/login",
[body("username").notEmpty(), body("password").notEmpty(), validate],
async (req, res, next) => {
try {
const { username, password } = req.body;
const result = await authService.login(
username,
password,
req.ip,
req.headers["user-agent"]
);
res.json(result);
} catch (error) {
next(error);
}
}
);
router.post("/logout", authMiddleware, async (req, res, next) => {
try {
await authService.logout(req.user.id, req.ip, req.headers["user-agent"]);
res.json({ message: "Successfully logged out" });
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@ -0,0 +1,180 @@
// src/controllers/admin/companies/branches.controller.js
const express = require("express");
const router = express.Router();
const branchService = require("../../../services/branch.service");
const authMiddleware = require("../../../middleware/auth.middleware");
const roleCheck = require("../../../middleware/roleCheck.middleware");
const { body } = require("express-validator");
const validate = require("../../../middleware/validator.middleware");
router.use(authMiddleware);
router.use(roleCheck(["super_admin", "company_admin"]));
// Get all branches (filtered by company for company_admin)
router.get("/", async (req, res, next) => {
try {
let filters = {};
// Company admins can only see their company's branches
if (req.user.role === "company_admin") {
filters.companyId = req.user.companyId;
} else if (req.query.companyId) {
// Super admin can filter by company
filters.companyId = req.query.companyId;
}
const branches = await branchService.findAll(filters);
res.json(branches);
} catch (error) {
next(error);
}
});
// Create new branch
router.post(
"/",
[
body("name").notEmpty().withMessage("Branch name is required"),
body("address").notEmpty().withMessage("Address is required"),
body("tel")
.notEmpty()
.matches(/^[0-9-]+$/)
.withMessage("Invalid telephone number format"),
body("companyId").isUUID().withMessage("Valid company ID is required"),
validate,
],
async (req, res, next) => {
try {
const branchData = req.body;
// Company admin can only create branches for their company
if (req.user.role === "company_admin") {
if (branchData.companyId !== req.user.companyId) {
return res.status(403).json({
message: "Can only create branches for your company",
});
}
}
const branch = await branchService.createBranch(branchData);
res.status(201).json(branch);
} catch (error) {
next(error);
}
}
);
// Get specific branch
router.get("/:id", async (req, res, next) => {
try {
const branch = await branchService.findById(req.params.id);
if (!branch) {
return res.status(404).json({ message: "Branch not found" });
}
// Check if company admin has access to this branch
if (
req.user.role === "company_admin" &&
branch.companyId !== req.user.companyId
) {
return res.status(403).json({ message: "Access denied" });
}
res.json(branch);
} catch (error) {
next(error);
}
});
// Update branch
router.put(
"/:id",
[
body("name").optional().notEmpty(),
body("address").optional().notEmpty(),
body("tel")
.optional()
.matches(/^[0-9-]+$/),
validate,
],
async (req, res, next) => {
try {
// Check if branch exists and admin has access
const branch = await branchService.findById(req.params.id);
if (!branch) {
return res.status(404).json({ message: "Branch not found" });
}
if (
req.user.role === "company_admin" &&
branch.companyId !== req.user.companyId
) {
return res.status(403).json({ message: "Access denied" });
}
// Don't allow changing companyId
delete req.body.companyId;
const updatedBranch = await branchService.updateBranch(
req.params.id,
req.body
);
res.json(updatedBranch);
} catch (error) {
next(error);
}
}
);
// Delete branch
router.delete("/:id", async (req, res, next) => {
try {
const branch = await branchService.findById(req.params.id);
if (!branch) {
return res.status(404).json({ message: "Branch not found" });
}
// Check if company admin has access to this branch
if (
req.user.role === "company_admin" &&
branch.companyId !== req.user.companyId
) {
return res.status(403).json({ message: "Access denied" });
}
await branchService.deleteBranch(req.params.id);
res.json({ message: "Branch successfully deleted" });
} catch (error) {
next(error);
}
});
// Deactivate branch
router.put("/:id/deactivate", async (req, res, next) => {
try {
const branch = await branchService.findById(req.params.id);
if (!branch) {
return res.status(404).json({ message: "Branch not found" });
}
if (
req.user.role === "company_admin" &&
branch.companyId !== req.user.companyId
) {
return res.status(403).json({ message: "Access denied" });
}
const updatedBranch = await branchService.updateBranch(req.params.id, {
isActive: false,
});
res.json(updatedBranch);
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@ -0,0 +1,83 @@
// src/controllers/admin/companies/companies.controller.js
const express = require('express');
const router = express.Router();
const companyService = require('../../../services/company.service');
const authMiddleware = require('../../../middleware/auth.middleware');
const roleCheck = require('../../../middleware/roleCheck.middleware');
const { body } = require('express-validator');
const validate = require('../../../middleware/validator.middleware');
router.use(authMiddleware);
router.use(roleCheck(['super_admin']));
router.post('/',
[
body('name').notEmpty(),
body('businessNumber').notEmpty(),
body('address').notEmpty(),
body('tel').notEmpty(),
body('email').isEmail(),
body('representative').notEmpty(),
validate
],
async (req, res, next) => {
try {
const company = await companyService.createCompany(req.body);
res.status(201).json(company);
} catch (error) {
next(error);
}
}
);
router.get('/', async (req, res, next) => {
try {
const companies = await companyService.findAll();
res.json(companies);
} catch (error) {
next(error);
}
});
router.get('/:id', async (req, res, next) => {
try {
const company = await companyService.findById(req.params.id);
if (!company) {
return res.status(404).json({ message: 'Company not found' });
}
res.json(company);
} catch (error) {
next(error);
}
});
router.put('/:id',
[
body('name').optional(),
body('businessNumber').optional(),
body('address').optional(),
body('tel').optional(),
body('email').optional().isEmail(),
body('representative').optional(),
validate
],
async (req, res, next) => {
try {
const company = await companyService.updateCompany(req.params.id, req.body);
res.json(company);
} catch (error) {
next(error);
}
}
);
router.delete('/:id', async (req, res, next) => {
try {
await companyService.deleteCompany(req.params.id);
res.json({ message: 'Company successfully deleted' });
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@ -0,0 +1,216 @@
// src/controllers/app/users/users.controller.js
const express = require("express");
const router = express.Router();
const bcrypt = require("bcrypt");
const userService = require("../../../services/user.service");
const authMiddleware = require("../../../middleware/auth.middleware");
const roleCheck = require("../../../middleware/roleCheck.middleware");
const { body } = require("express-validator");
const validate = require("../../../middleware/validator.middleware");
router.use(authMiddleware);
// Get current user profile
router.get("/me", async (req, res, next) => {
try {
const user = await userService.findById(req.user.id);
res.json(user);
} catch (error) {
next(error);
}
});
// Update current user profile
router.put(
"/me",
[
body("name").optional().isString(),
body("email").optional().isEmail(),
body("phone")
.optional()
.matches(/^[0-9-]+$/),
body("password")
.optional()
.isLength({ min: 8 })
.matches(
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/
),
validate,
],
async (req, res, next) => {
try {
// password와 email만 분리 (별도 처리가 필요한 필드만 분리)
const { password, email, ...updateData } = req.body;
// role 수정 시도를 방지
delete updateData.role; // role 필드가 있다면 제거
// 추가로 수정하면 안 되는 필드들도 제거
delete updateData.isActive;
delete updateData.companyId;
delete updateData.branchId;
// 비밀번호 변경이 있는 경우
if (password) {
updateData.password = await bcrypt.hash(password, 10);
}
// 이메일 변경이 있는 경우
if (email) {
const existingUser = await userService.findByEmail(email);
if (existingUser && existingUser.id !== req.user.id) {
return res.status(400).json({
message: "Email already in use",
});
}
updateData.email = email;
}
const updatedUser = await userService.updateUser(req.user.id, updateData);
res.json(updatedUser);
} catch (error) {
next(error);
}
}
);
// The following routes require admin privileges
router.use(roleCheck(["super_admin", "company_admin", "branch_admin"]));
// Get users (filtered by company/branch based on user role)
router.get("/", async (req, res, next) => {
try {
const filters = {};
// Filter users based on role
if (req.user.role === "company_admin") {
filters.companyId = req.user.companyId;
} else if (req.user.role === "branch_admin") {
filters.branchId = req.user.branchId;
}
const users = await userService.findAll(filters);
res.json(users);
} catch (error) {
next(error);
}
});
// Create new user (super_admin and company_admin only)
router.post(
"/",
roleCheck(["super_admin", "company_admin"]),
[
body("username").notEmpty().isLength({ min: 4 }),
body("password")
.notEmpty()
.isLength({ min: 8 })
.matches(
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/
),
body("name").notEmpty(),
body("email").isEmail(),
body("phone").matches(/^[0-9-]+$/),
body("role").isIn(["company_admin", "branch_admin", "user"]),
body("companyId").optional().isUUID(),
body("branchId").optional().isUUID(),
validate,
],
async (req, res, next) => {
try {
const userData = req.body;
// Company admin can only create users for their company
if (req.user.role === "company_admin") {
userData.companyId = req.user.companyId;
if (userData.role === "company_admin") {
return res
.status(403)
.json({ message: "Cannot create company admin" });
}
}
const user = await userService.createUser(userData);
res.status(201).json(user);
} catch (error) {
next(error);
}
}
);
// Get specific user
router.get("/:id", async (req, res, next) => {
try {
const user = await userService.findById(req.params.id);
// Check if user belongs to admin's company/branch
if (
req.user.role === "company_admin" &&
user.companyId !== req.user.companyId
) {
return res.status(403).json({ message: "Access denied" });
}
if (
req.user.role === "branch_admin" &&
user.branchId !== req.user.branchId
) {
return res.status(403).json({ message: "Access denied" });
}
res.json(user);
} catch (error) {
next(error);
}
});
// Update user
router.put(
"/:id",
roleCheck(["super_admin", "company_admin"]),
[
body("name").optional().isString(),
body("email").optional().isEmail(),
body("phone")
.optional()
.matches(/^[0-9-]+$/),
body("role").optional().isIn(["company_admin", "branch_admin", "user"]),
body("isActive").optional().isBoolean(),
validate,
],
async (req, res, next) => {
try {
// Check permissions
const targetUser = await userService.findById(req.params.id);
if (req.user.role === "company_admin") {
if (targetUser.companyId !== req.user.companyId) {
return res.status(403).json({ message: "Access denied" });
}
if (
targetUser.role === "company_admin" ||
req.body.role === "company_admin"
) {
return res
.status(403)
.json({ message: "Cannot modify company admin" });
}
}
const updatedUser = await userService.updateUser(req.params.id, req.body);
res.json(updatedUser);
} catch (error) {
next(error);
}
}
);
// Delete user (super_admin only)
router.delete("/:id", roleCheck(["super_admin"]), async (req, res, next) => {
try {
await userService.deleteUser(req.params.id);
res.json({ message: "User successfully deleted" });
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@ -0,0 +1,32 @@
// src/middleware/auth.middleware.js
const jwt = require('jsonwebtoken');
const config = require('../config/config');
const { User } = require('../models');
const authMiddleware = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Authentication token is required' });
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, config.jwt.secret);
const user = await User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (!user || !user.isActive) {
return res.status(401).json({ message: 'User not found or inactive' });
}
req.user = user;
next();
} catch (error) {
return res.status(401).json({ message: 'Invalid token' });
}
};
module.exports = authMiddleware;

View File

@ -0,0 +1,34 @@
// src/middleware/errorHandler.middleware.js
const logger = require('../config/logger');
const errorHandler = (err, req, res, next) => {
logger.error(err.stack);
if (err.name === 'SequelizeValidationError') {
return res.status(400).json({
message: 'Validation error',
errors: err.errors.map(e => ({
field: e.path,
message: e.message
}))
});
}
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
message: 'Duplicate entry',
errors: err.errors.map(e => ({
field: e.path,
message: e.message
}))
});
}
res.status(500).json({
message: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
};
module.exports = errorHandler;

View File

@ -0,0 +1,22 @@
// src/middleware/requestLogger.middleware.js
const logger = require('../config/logger');
const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.headers['user-agent']
});
});
next();
};
module.exports = requestLogger;

View File

@ -0,0 +1,16 @@
// src/middleware/roleCheck.middleware.js
const roleCheck = (roles = []) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: 'Unauthorized' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden: Insufficient privileges' });
}
next();
};
};
module.exports = roleCheck;

View File

@ -0,0 +1,15 @@
// src/middleware/validator.middleware.js
const { validationResult } = require("express-validator");
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
message: "Validation error",
errors: errors.array(),
});
}
next();
};
module.exports = validate;

View File

@ -0,0 +1,23 @@
// src/models/AuthLog.js
const { DataTypes } = require("sequelize");
const sequelize = require("../config/database");
const AuthLog = sequelize.define("AuthLog", {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
action: {
type: DataTypes.STRING,
allowNull: false,
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true,
},
userAgent: {
type: DataTypes.STRING,
allowNull: true,
},
});

View File

@ -0,0 +1,27 @@
// src/models/Branch.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const Branch = sequelize.define('Branch', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING(100),
allowNull: false
},
address: {
type: DataTypes.STRING(100),
allowNull: false
},
tel: {
type: DataTypes.STRING(20),
allowNull: false
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
});

View File

@ -0,0 +1,58 @@
// src/models/Company.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const Company = sequelize.define('Company', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING(100),
allowNull: false
},
businessNumber: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true
},
address: {
type: DataTypes.STRING(100),
allowNull: false
},
tel: {
type: DataTypes.STRING(20),
allowNull: false
},
email: {
type: DataTypes.STRING(50),
allowNull: false,
validate: {
isEmail: true
}
},
representative: {
type: DataTypes.STRING(50),
allowNull: false
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
contractStartDate: {
type: DataTypes.DATEONLY,
allowNull: true
},
contractEndDate: {
type: DataTypes.DATEONLY,
allowNull: true
}
});

View File

@ -0,0 +1,66 @@
// src/models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const bcrypt = require('bcryptjs');
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
name: {
type: DataTypes.STRING(50),
allowNull: false
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
phone: {
type: DataTypes.STRING(20),
allowNull: false
},
role: {
type: DataTypes.ENUM('super_admin', 'company_admin', 'branch_admin', 'user'),
defaultValue: 'user'
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
lastLoginAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
hooks: {
beforeCreate: async (user) => {
if (user.password) {
user.password = await bcrypt.hash(user.password, 10);
}
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
user.password = await bcrypt.hash(user.password, 10);
}
}
}
});
User.prototype.validatePassword = function(password) {
return bcrypt.compare(password, this.password);
};

View File

@ -0,0 +1,27 @@
// src/models/index.js
const sequelize = require("../config/database");
const Company = require("./Company");
const Branch = require("./Branch");
const User = require("./User");
const AuthLog = require("./AuthLog");
// Define relationships
Company.hasMany(Branch);
Branch.belongsTo(Company);
Company.hasMany(User);
User.belongsTo(Company);
Branch.hasMany(User);
User.belongsTo(Branch);
User.hasMany(AuthLog);
AuthLog.belongsTo(User);
module.exports = {
sequelize,
Company,
Branch,
User,
AuthLog,
};

View File

@ -0,0 +1,13 @@
// src/routes/admin.js
const express = require('express');
const router = express.Router();
// Import admin controllers
const companiesController = require('./controllers/admin/companies/companies.controller');
const branchesController = require('./controllers/admin/companies/branches.controller');
// Mount admin routes
router.use('/companies', companiesController);
router.use('/branches', branchesController);
module.exports = router;

View File

@ -0,0 +1,13 @@
// src/routes/app.js
const express = require('express');
const router = express.Router();
// Import app controllers
const authController = require('./controllers/app/auth/auth.controller');
const usersController = require('./controllers/app/users/users.controller');
// Mount app routes
router.use('/auth', authController);
router.use('/users', usersController);
module.exports = router;

View File

@ -0,0 +1,68 @@
// src/services/auth.service.js
const jwt = require("jsonwebtoken");
const config = require("../config/config");
const { User, AuthLog } = require("../models");
class AuthService {
async login(username, password, ipAddress, userAgent) {
const user = await User.findOne({ where: { username } });
if (!user || !user.isActive) {
await this._logAuthAttempt(
user?.id,
"failed_login",
ipAddress,
userAgent
);
throw new Error("Invalid credentials");
}
const isValidPassword = await user.validatePassword(password);
if (!isValidPassword) {
await this._logAuthAttempt(user.id, "failed_login", ipAddress, userAgent);
throw new Error("Invalid credentials");
}
await User.update({ lastLoginAt: new Date() }, { where: { id: user.id } });
await this._logAuthAttempt(user.id, "login", ipAddress, userAgent);
const token = this._generateToken(user);
const userWithoutPassword = { ...user.toJSON() };
delete userWithoutPassword.password;
return {
token,
user: userWithoutPassword,
};
}
async logout(userId, ipAddress, userAgent) {
await this._logAuthAttempt(userId, "logout", ipAddress, userAgent);
return true;
}
async _logAuthAttempt(userId, action, ipAddress, userAgent) {
await AuthLog.create({
userId,
action,
ipAddress,
userAgent,
});
}
_generateToken(user) {
return jwt.sign(
{
id: user.id,
role: user.role,
companyId: user.companyId,
branchId: user.branchId,
},
config.jwt.secret,
{ expiresIn: config.jwt.expiresIn }
);
}
}
module.exports = new AuthService();

View File

@ -0,0 +1,50 @@
// src/services/branch.service.js
const { Branch, Company, User } = require("../models");
class BranchService {
async createBranch(branchData) {
return await Branch.create(branchData);
}
async findById(id) {
return await Branch.findByPk(id, {
include: [
{ model: Company },
{
model: User,
attributes: { exclude: ["password"] },
},
],
});
}
async findAll(filters = {}) {
return await Branch.findAll({
where: filters,
include: [
{ model: Company },
{
model: User,
attributes: { exclude: ["password"] },
},
],
});
}
async updateBranch(id, updateData) {
const branch = await Branch.findByPk(id);
if (!branch) throw new Error("Branch not found");
return await branch.update(updateData);
}
async deleteBranch(id) {
const branch = await Branch.findByPk(id);
if (!branch) throw new Error("Branch not found");
await branch.destroy();
return true;
}
}
module.exports = new BranchService();

View File

@ -0,0 +1,50 @@
// src/services/company.service.js
const { Company, Branch, User } = require("../models");
class CompanyService {
async createCompany(companyData) {
return await Company.create(companyData);
}
async findById(id) {
return await Company.findByPk(id, {
include: [
{ model: Branch },
{
model: User,
attributes: { exclude: ["password"] },
},
],
});
}
async findAll(filters = {}) {
return await Company.findAll({
where: filters,
include: [
{ model: Branch },
{
model: User,
attributes: { exclude: ["password"] },
},
],
});
}
async updateCompany(id, updateData) {
const company = await Company.findByPk(id);
if (!company) throw new Error("Company not found");
return await company.update(updateData);
}
async deleteCompany(id) {
const company = await Company.findByPk(id);
if (!company) throw new Error("Company not found");
await company.destroy();
return true;
}
}
module.exports = new CompanyService();

View File

@ -0,0 +1,40 @@
// src/services/user.service.js
const { User, Company, Branch } = require("../models");
class UserService {
async createUser(userData) {
return await User.create(userData);
}
async findById(id) {
return await User.findByPk(id, {
attributes: { exclude: ["password"] },
include: [{ model: Company }, { model: Branch }],
});
}
async findAll(filters = {}) {
return await User.findAll({
where: filters,
attributes: { exclude: ["password"] },
include: [{ model: Company }, { model: Branch }],
});
}
async updateUser(id, updateData) {
const user = await User.findByPk(id);
if (!user) throw new Error("User not found");
return await user.update(updateData);
}
async deleteUser(id) {
const user = await User.findByPk(id);
if (!user) throw new Error("User not found");
await user.destroy();
return true;
}
}
module.exports = new UserService();

View File

@ -0,0 +1,7 @@
// src/utils/asyncHandler.js
// Utility function to handle async route handlers
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;

View File

@ -0,0 +1,28 @@
// src/utils/createInitialAdmin.js
const { User } = require('../models');
const logger = require('../config/logger');
const createInitialAdmin = async () => {
try {
const existingAdmin = await User.findOne({
where: { role: 'super_admin' }
});
if (!existingAdmin) {
await User.create({
username: 'admin',
password: 'Admin123!@#', // This should be changed immediately after first login
name: 'System Administrator',
email: 'admin@fems.com',
phone: '010-0000-0000',
role: 'super_admin',
isActive: true
});
logger.info('Initial super admin created successfully');
}
} catch (error) {
logger.error('Error creating initial admin:', error);
}
};
module.exports = createInitialAdmin;

View File

@ -0,0 +1,58 @@
// src/utils/validators.js
const { body } = require("express-validator");
const companyValidators = {
create: [
body("name")
.notEmpty()
.withMessage("Company name is required")
.isLength({ max: 100 })
.withMessage("Company name must be less than 100 characters"),
body("businessNumber")
.notEmpty()
.withMessage("Business number is required")
.matches(/^[0-9-]+$/)
.withMessage("Invalid business number format"),
body("address").notEmpty().withMessage("Address is required"),
body("tel")
.notEmpty()
.withMessage("Telephone number is required")
.matches(/^[0-9-]+$/)
.withMessage("Invalid telephone number format"),
body("email").isEmail().withMessage("Invalid email format"),
body("representative")
.notEmpty()
.withMessage("Representative name is required"),
],
};
const userValidators = {
create: [
body("username")
.notEmpty()
.withMessage("Username is required")
.isLength({ min: 4 })
.withMessage("Username must be at least 4 characters"),
body("password")
.notEmpty()
.withMessage("Password is required")
.isLength({ min: 8 })
.withMessage("Password must be at least 8 characters")
.matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/)
.withMessage(
"Password must contain at least one letter, one number and one special character"
),
body("email").isEmail().withMessage("Invalid email format"),
body("phone")
.matches(/^[0-9-]+$/)
.withMessage("Invalid phone number format"),
body("role")
.isIn(["super_admin", "company_admin", "branch_admin", "user"])
.withMessage("Invalid role"),
],
};
module.exports = {
companyValidators,
userValidators,
};

3700
fems-api/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2732
fems-app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff