auto commit
This commit is contained in:
parent
208ba0d1cb
commit
1a7cfe9c0e
20
README.md
20
README.md
@ -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
|
||||
|
||||
|
@ -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}"]
|
||||
|
5170
fems-admin/package-lock.json
generated
5170
fems-admin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2732
fems-admin/yarn.lock
Normal file
2732
fems-admin/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
13
fems-api/.eslintrc.json
Normal file
13
fems-api/.eslintrc.json
Normal 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
8
fems-api/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# .gitignore
|
||||
node_modules/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
logs/
|
||||
coverage/
|
||||
.DS_Store
|
32
fems-api/package.json
Normal file
32
fems-api/package.json
Normal 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
56
fems-api/src/app.js
Normal 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;
|
45
fems-api/src/config/config.js
Normal file
45
fems-api/src/config/config.js
Normal 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"];
|
22
fems-api/src/config/database.js
Normal file
22
fems-api/src/config/database.js
Normal 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;
|
||||
|
33
fems-api/src/config/logger.js
Normal file
33
fems-api/src/config/logger.js
Normal 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;
|
37
fems-api/src/controllers/app/auth/auth.controller.js
Normal file
37
fems-api/src/controllers/app/auth/auth.controller.js
Normal 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;
|
180
fems-api/src/controllers/app/auth/branches.controller.js
Normal file
180
fems-api/src/controllers/app/auth/branches.controller.js
Normal 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;
|
83
fems-api/src/controllers/app/auth/companies.controller.js
Normal file
83
fems-api/src/controllers/app/auth/companies.controller.js
Normal 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;
|
216
fems-api/src/controllers/app/auth/users.controller.js
Normal file
216
fems-api/src/controllers/app/auth/users.controller.js
Normal 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;
|
32
fems-api/src/middleware/auth.middleware.js
Normal file
32
fems-api/src/middleware/auth.middleware.js
Normal 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;
|
||||
|
34
fems-api/src/middleware/errorHandler.middleware.js
Normal file
34
fems-api/src/middleware/errorHandler.middleware.js
Normal 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;
|
22
fems-api/src/middleware/requestLogger.middleware.js
Normal file
22
fems-api/src/middleware/requestLogger.middleware.js
Normal 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;
|
16
fems-api/src/middleware/roleCheck.middleware.js
Normal file
16
fems-api/src/middleware/roleCheck.middleware.js
Normal 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;
|
15
fems-api/src/middleware/validator.middleware.js
Normal file
15
fems-api/src/middleware/validator.middleware.js
Normal 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;
|
23
fems-api/src/models/AuthLog.js
Normal file
23
fems-api/src/models/AuthLog.js
Normal 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,
|
||||
},
|
||||
});
|
27
fems-api/src/models/Branch.js
Normal file
27
fems-api/src/models/Branch.js
Normal 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
|
||||
}
|
||||
});
|
58
fems-api/src/models/Company.js
Normal file
58
fems-api/src/models/Company.js
Normal 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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
66
fems-api/src/models/User.js
Normal file
66
fems-api/src/models/User.js
Normal 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);
|
||||
};
|
27
fems-api/src/models/index.js
Normal file
27
fems-api/src/models/index.js
Normal 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,
|
||||
};
|
13
fems-api/src/routes/admin.js
Normal file
13
fems-api/src/routes/admin.js
Normal 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;
|
13
fems-api/src/routes/app.js
Normal file
13
fems-api/src/routes/app.js
Normal 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;
|
68
fems-api/src/services/auth.service.js
Normal file
68
fems-api/src/services/auth.service.js
Normal 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();
|
50
fems-api/src/services/branch.service.js
Normal file
50
fems-api/src/services/branch.service.js
Normal 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();
|
50
fems-api/src/services/company.service.js
Normal file
50
fems-api/src/services/company.service.js
Normal 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();
|
40
fems-api/src/services/user.service.js
Normal file
40
fems-api/src/services/user.service.js
Normal 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();
|
7
fems-api/src/utils/asyncHandler.js
Normal file
7
fems-api/src/utils/asyncHandler.js
Normal 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;
|
28
fems-api/src/utils/createInitialAdmin.js
Normal file
28
fems-api/src/utils/createInitialAdmin.js
Normal 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;
|
58
fems-api/src/utils/validators.js
Normal file
58
fems-api/src/utils/validators.js
Normal 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
3700
fems-api/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
5170
fems-app/package-lock.json
generated
5170
fems-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2732
fems-app/yarn.lock
Normal file
2732
fems-app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user