- Integrated express-async-errors to automatically handle errors in async route handlers, enhancing the overall error management in the application. - Updated app.ts to include the express-async-errors import for global error handling. - Removed redundant logging statements in admin and user menu retrieval functions to streamline the code and improve readability. - Adjusted logging levels from info to debug for less critical logs, ensuring that important information is logged appropriately without cluttering the logs.
542 lines
17 KiB
TypeScript
542 lines
17 KiB
TypeScript
// 인증 컨트롤러
|
|
// 기존 Java ApiLoginController를 Node.js로 포팅
|
|
|
|
import { Request, Response } from "express";
|
|
import { AuthService } from "../services/authService";
|
|
import { JwtUtils } from "../utils/jwtUtils";
|
|
import { LoginRequest, UserInfo, ApiResponse, PersonBean } from "../types/auth";
|
|
import { logger } from "../utils/logger";
|
|
|
|
export class AuthController {
|
|
/**
|
|
* POST /api/auth/login
|
|
* 기존 Java ApiLoginController.login() 메서드 포팅
|
|
*/
|
|
static async login(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { userId, password }: LoginRequest = req.body;
|
|
const remoteAddr = req.ip || req.connection.remoteAddress || "unknown";
|
|
|
|
logger.debug(`로그인 요청: ${userId}`);
|
|
|
|
// 입력값 검증
|
|
if (!userId || !password) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "사용자 ID와 비밀번호를 입력해주세요.",
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "필수 입력값이 누락되었습니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 로그인 프로세스 실행
|
|
const loginResult = await AuthService.processLogin(
|
|
userId,
|
|
password,
|
|
remoteAddr
|
|
);
|
|
|
|
if (loginResult.success && loginResult.userInfo && loginResult.token) {
|
|
// 로그인 성공
|
|
const userInfo: UserInfo = {
|
|
userId: loginResult.userInfo.userId,
|
|
userName: loginResult.userInfo.userName || "",
|
|
deptName: loginResult.userInfo.deptName || "",
|
|
companyCode: loginResult.userInfo.companyCode || "ILSHIN",
|
|
};
|
|
|
|
logger.debug(`로그인 사용자 정보: ${userInfo.userId} (${userInfo.companyCode})`);
|
|
|
|
// 사용자의 첫 번째 접근 가능한 메뉴 조회
|
|
let firstMenuPath: string | null = null;
|
|
try {
|
|
const { AdminService } = await import("../services/adminService");
|
|
const paramMap = {
|
|
userId: loginResult.userInfo.userId,
|
|
userCompanyCode: loginResult.userInfo.companyCode || "ILSHIN",
|
|
userType: loginResult.userInfo.userType,
|
|
userLang: "ko",
|
|
};
|
|
|
|
const menuList = await AdminService.getUserMenuList(paramMap);
|
|
logger.debug(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
|
|
|
|
// 접근 가능한 첫 번째 메뉴 찾기
|
|
// 조건:
|
|
// 1. LEV (레벨)이 2 이상 (최상위 폴더 제외)
|
|
// 2. MENU_URL이 있고 비어있지 않음
|
|
// 3. 이미 PATH, SEQ로 정렬되어 있으므로 첫 번째로 찾은 것이 첫 번째 메뉴
|
|
const firstMenu = menuList.find((menu: any) => {
|
|
const level = menu.lev || menu.level;
|
|
const url = menu.menu_url || menu.url;
|
|
|
|
return level >= 2 && url && url.trim() !== "" && url !== "#";
|
|
});
|
|
|
|
if (firstMenu) {
|
|
firstMenuPath = firstMenu.menu_url || firstMenu.url;
|
|
logger.debug(`첫 번째 메뉴: ${firstMenuPath}`);
|
|
} else {
|
|
logger.debug("접근 가능한 메뉴 없음, 메인 페이지로 이동");
|
|
}
|
|
} catch (menuError) {
|
|
logger.warn("메뉴 조회 중 오류 발생 (무시하고 계속):", menuError);
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "로그인 성공",
|
|
data: {
|
|
userInfo,
|
|
token: loginResult.token,
|
|
firstMenuPath, // 첫 번째 접근 가능한 메뉴 경로 추가
|
|
},
|
|
});
|
|
} else {
|
|
// 로그인 실패
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "로그인 실패",
|
|
error: {
|
|
code: "LOGIN_FAILED",
|
|
details:
|
|
loginResult.errorReason || "알 수 없는 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 API 오류: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "서버 오류가 발생했습니다.",
|
|
error: {
|
|
code: "SERVER_ERROR",
|
|
details:
|
|
error instanceof Error
|
|
? error.message
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/auth/switch-company
|
|
* WACE 관리자 전용: 다른 회사로 전환
|
|
*/
|
|
static async switchCompany(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { companyCode } = req.body;
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (!token) {
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "인증 토큰이 필요합니다.",
|
|
error: { code: "TOKEN_MISSING" },
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 현재 사용자 정보 확인
|
|
const currentUser = JwtUtils.verifyToken(token);
|
|
|
|
// WACE 관리자 권한 체크 (userType = "SUPER_ADMIN"만 확인)
|
|
// 이미 다른 회사로 전환한 상태(companyCode != "*")에서도 다시 전환 가능해야 함
|
|
if (currentUser.userType !== "SUPER_ADMIN") {
|
|
logger.warn(`회사 전환 권한 없음: userId=${currentUser.userId}, userType=${currentUser.userType}, companyCode=${currentUser.companyCode}`);
|
|
res.status(403).json({
|
|
success: false,
|
|
message: "회사 전환은 최고 관리자(SUPER_ADMIN)만 가능합니다.",
|
|
error: { code: "FORBIDDEN" },
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 전환할 회사 코드 검증
|
|
if (!companyCode || companyCode.trim() === "") {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "전환할 회사 코드가 필요합니다.",
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info(`=== WACE 관리자 회사 전환 ===`, {
|
|
userId: currentUser.userId,
|
|
originalCompanyCode: currentUser.companyCode,
|
|
targetCompanyCode: companyCode,
|
|
});
|
|
|
|
// 회사 코드 존재 여부 확인 (company_code가 "*"가 아닌 경우만)
|
|
if (companyCode !== "*") {
|
|
const { query } = await import("../database/db");
|
|
const companies = await query<any>(
|
|
"SELECT company_code, company_name FROM company_mng WHERE company_code = $1",
|
|
[companyCode]
|
|
);
|
|
|
|
if (companies.length === 0) {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: "존재하지 않는 회사 코드입니다.",
|
|
error: { code: "COMPANY_NOT_FOUND" },
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 새로운 JWT 토큰 발급 (company_code만 변경)
|
|
const newPersonBean: PersonBean = {
|
|
...currentUser,
|
|
companyCode: companyCode.trim(), // 전환할 회사 코드로 변경
|
|
};
|
|
|
|
const newToken = JwtUtils.generateToken(newPersonBean);
|
|
|
|
logger.info(`✅ 회사 전환 성공: ${currentUser.userId} → ${companyCode}`);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "회사 전환 완료",
|
|
data: {
|
|
token: newToken,
|
|
companyCode: companyCode.trim(),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`회사 전환 API 오류: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "회사 전환 중 오류가 발생했습니다.",
|
|
error: {
|
|
code: "SERVER_ERROR",
|
|
details:
|
|
error instanceof Error
|
|
? error.message
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/auth/logout
|
|
* 기존 Java ApiLoginController.logout() 메서드 포팅
|
|
*/
|
|
static async logout(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const remoteAddr = req.ip || req.connection.remoteAddress || "unknown";
|
|
|
|
// JWT 토큰에서 사용자 정보 추출
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (token) {
|
|
try {
|
|
const userInfo = JwtUtils.verifyToken(token);
|
|
await AuthService.processLogout(userInfo.userId, remoteAddr);
|
|
} catch (tokenError) {
|
|
logger.warn(
|
|
`로그아웃 시 토큰 검증 실패: ${tokenError instanceof Error ? tokenError.message : tokenError}`
|
|
);
|
|
}
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "로그아웃되었습니다.",
|
|
data: null,
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그아웃 API 오류: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "로그아웃 처리 중 오류가 발생했습니다.",
|
|
error: {
|
|
code: "LOGOUT_ERROR",
|
|
details:
|
|
error instanceof Error
|
|
? error.message
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/auth/me
|
|
* 기존 Java ApiLoginController.getCurrentUser() 메서드 포팅
|
|
*/
|
|
static async getCurrentUser(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (!token) {
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "인증되지 않은 사용자입니다.",
|
|
error: {
|
|
code: "NOT_AUTHENTICATED",
|
|
details: "세션이 만료되었거나 로그인이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const userInfo = JwtUtils.verifyToken(token);
|
|
|
|
// DB에서 최신 사용자 정보 조회 (locale 포함)
|
|
const dbUserInfo = await AuthService.getUserInfo(userInfo.userId);
|
|
|
|
if (!dbUserInfo) {
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "사용자 정보를 찾을 수 없습니다.",
|
|
error: {
|
|
code: "USER_NOT_FOUND",
|
|
details: "사용자 정보가 삭제되었거나 존재하지 않습니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 프론트엔드 호환성을 위해 더 많은 사용자 정보 반환
|
|
// ⚠️ JWT 토큰의 companyCode를 우선 사용 (회사 전환 기능 지원)
|
|
const userInfoResponse: any = {
|
|
userId: dbUserInfo.userId,
|
|
userName: dbUserInfo.userName || "",
|
|
deptName: dbUserInfo.deptName || "",
|
|
companyCode: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
|
|
company_code: userInfo.companyCode || dbUserInfo.companyCode || "ILSHIN", // JWT 토큰 우선
|
|
userType: userInfo.userType || dbUserInfo.userType || "USER", // JWT 토큰 우선
|
|
userTypeName: dbUserInfo.userTypeName || "일반사용자",
|
|
email: dbUserInfo.email || "",
|
|
photo: dbUserInfo.photo,
|
|
locale: dbUserInfo.locale || "KR", // locale 정보 추가
|
|
deptCode: dbUserInfo.deptCode, // 추가 필드
|
|
isAdmin:
|
|
dbUserInfo.userType === "ADMIN" || dbUserInfo.userId === "plm_admin",
|
|
};
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "사용자 정보 조회 성공",
|
|
data: userInfoResponse,
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`사용자 정보 조회 API 오류: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "인증되지 않은 사용자입니다.",
|
|
error: {
|
|
code: "NOT_AUTHENTICATED",
|
|
details: "세션이 만료되었거나 로그인이 필요합니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/auth/status
|
|
* 기존 Java ApiLoginController.checkAuthStatus() 메서드 포팅
|
|
*/
|
|
static async checkAuthStatus(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (!token) {
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "세션 상태 확인",
|
|
data: {
|
|
isLoggedIn: false,
|
|
isAdmin: false,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const validation = JwtUtils.validateToken(token);
|
|
|
|
if (!validation.isValid) {
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "세션 상태 확인",
|
|
data: {
|
|
isLoggedIn: false,
|
|
isAdmin: false,
|
|
error: validation.error,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 토큰에서 사용자 정보 추출하여 관리자 권한 확인
|
|
let isAdmin = false;
|
|
try {
|
|
const userInfo = JwtUtils.verifyToken(token);
|
|
// 기존 Java 로직과 동일: plm_admin 사용자만 관리자로 인식
|
|
isAdmin =
|
|
userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN";
|
|
|
|
logger.info(`인증 상태 확인: ${userInfo.userId}, 관리자: ${isAdmin}`);
|
|
} catch (error) {
|
|
logger.error(`토큰에서 사용자 정보 추출 실패: ${error}`);
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "세션 상태 확인",
|
|
data: {
|
|
isLoggedIn: true,
|
|
isAdmin: isAdmin,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`세션 상태 확인 API 오류: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "세션 상태 확인 중 오류가 발생했습니다.",
|
|
error: {
|
|
code: "SESSION_CHECK_ERROR",
|
|
details:
|
|
error instanceof Error
|
|
? error.message
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/auth/refresh
|
|
* JWT 토큰 갱신 API
|
|
*/
|
|
static async refreshToken(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (!token) {
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "토큰이 필요합니다.",
|
|
error: {
|
|
code: "TOKEN_MISSING",
|
|
details: "인증 토큰이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const newToken = JwtUtils.refreshToken(token);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "토큰 갱신 성공",
|
|
data: {
|
|
token: newToken,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`토큰 갱신 API 오류: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
res.status(401).json({
|
|
success: false,
|
|
message: "토큰 갱신에 실패했습니다.",
|
|
error: {
|
|
code: "TOKEN_REFRESH_ERROR",
|
|
details:
|
|
error instanceof Error
|
|
? error.message
|
|
: "알 수 없는 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/auth/signup
|
|
* 공차중계 회원가입 API
|
|
*/
|
|
static async signup(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { userId, password, userName, phoneNumber, licenseNumber, vehicleNumber, vehicleType } = req.body;
|
|
|
|
logger.info(`=== 공차중계 회원가입 API 호출 ===`);
|
|
logger.info(`userId: ${userId}, vehicleNumber: ${vehicleNumber}`);
|
|
|
|
// 입력값 검증
|
|
if (!userId || !password || !userName || !phoneNumber || !licenseNumber || !vehicleNumber) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: "필수 입력값이 누락되었습니다.",
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "아이디, 비밀번호, 이름, 연락처, 면허번호, 차량번호는 필수입니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 회원가입 처리
|
|
const signupResult = await AuthService.signupDriver({
|
|
userId,
|
|
password,
|
|
userName,
|
|
phoneNumber,
|
|
licenseNumber,
|
|
vehicleNumber,
|
|
vehicleType,
|
|
});
|
|
|
|
if (signupResult.success) {
|
|
logger.info(`공차중계 회원가입 성공: ${userId}`);
|
|
res.status(201).json({
|
|
success: true,
|
|
message: "회원가입이 완료되었습니다.",
|
|
});
|
|
} else {
|
|
logger.warn(`공차중계 회원가입 실패: ${userId} - ${signupResult.message}`);
|
|
res.status(400).json({
|
|
success: false,
|
|
message: signupResult.message || "회원가입에 실패했습니다.",
|
|
error: {
|
|
code: "SIGNUP_FAILED",
|
|
details: signupResult.message,
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error("공차중계 회원가입 API 오류:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "회원가입 처리 중 오류가 발생했습니다.",
|
|
error: {
|
|
code: "SIGNUP_ERROR",
|
|
details: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|