auto commit
This commit is contained in:
parent
208ba0d1cb
commit
1a7cfe9c0e
20
README.md
20
README.md
@ -24,6 +24,7 @@ wacefems/
|
|||||||
│ │ │ ├── support/ # 기술 지원
|
│ │ │ ├── support/ # 기술 지원
|
||||||
│ │ │ └── settings/ # 시스템 설정
|
│ │ │ └── settings/ # 시스템 설정
|
||||||
│ │ ├── components/
|
│ │ ├── components/
|
||||||
|
│ │ ├── lib/ # 라이브러리
|
||||||
│ │ ├── services/
|
│ │ ├── services/
|
||||||
│ │ └── types/
|
│ │ └── types/
|
||||||
│
|
│
|
||||||
@ -90,6 +91,7 @@ wacefems/
|
|||||||
│ │ │ ├── charts/ # 차트 컴포넌트
|
│ │ │ ├── charts/ # 차트 컴포넌트
|
||||||
│ │ │ └── forms/ # 폼 컴포넌트
|
│ │ │ └── forms/ # 폼 컴포넌트
|
||||||
│ │ │
|
│ │ │
|
||||||
|
│ │ ├── lib/ # 라이브러리
|
||||||
│ │ ├── hooks/ # 커스텀 훅
|
│ │ ├── hooks/ # 커스텀 훅
|
||||||
│ │ ├── services/ # API 서비스
|
│ │ ├── services/ # API 서비스
|
||||||
│ │ ├── stores/ # 상태 관리
|
│ │ ├── stores/ # 상태 관리
|
||||||
@ -111,6 +113,8 @@ wacefems/
|
|||||||
│ │ │ ├── monitoring/
|
│ │ │ ├── monitoring/
|
||||||
│ │ │ └── ...
|
│ │ │ └── ...
|
||||||
│ │ │
|
│ │ │
|
||||||
|
│ │ ├── routes/ # 라우터
|
||||||
|
│ │ ├── utils/ # 유틸리티
|
||||||
│ │ ├── models/ # 데이터 모델
|
│ │ ├── models/ # 데이터 모델
|
||||||
│ │ ├── services/ # 비즈니스 로직
|
│ │ ├── services/ # 비즈니스 로직
|
||||||
│ │ └── middleware/ # 미들웨어
|
│ │ └── middleware/ # 미들웨어
|
||||||
@ -124,7 +128,21 @@ wacefems/
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 기술 스택
|
### 2.2 데이터베이스 모델
|
||||||
|
|
||||||
|
## 데이터베이스 생성
|
||||||
|
|
||||||
|
```bash
|
||||||
|
createdb wacefems_database
|
||||||
|
```
|
||||||
|
|
||||||
|
### 데이터베이스 마이그레이션
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx sequelize-cli db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 기술 스택
|
||||||
|
|
||||||
#### Frontend
|
#### Frontend
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
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