344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
// 인증 서비스
|
|
// 기존 Java LoginService를 Node.js로 포팅
|
|
// ✅ Prisma → Raw Query 전환 완료 (Phase 1.5)
|
|
|
|
import { query } from "../database/db";
|
|
import { JwtUtils } from "../utils/jwtUtils";
|
|
import { EncryptUtil } from "../utils/encryptUtil";
|
|
import { PersonBean, LoginResult, LoginLogData } from "../types/auth";
|
|
import { logger } from "../utils/logger";
|
|
|
|
export class AuthService {
|
|
/**
|
|
* 기존 Java LoginService.loginPwdCheck() 메서드 포팅
|
|
* 로그인을 시도하여 결과를 return 한다.
|
|
*/
|
|
static async loginPwdCheck(
|
|
userId: string,
|
|
password: string
|
|
): Promise<LoginResult> {
|
|
try {
|
|
// 사용자 비밀번호 조회 (Raw Query 전환)
|
|
const result = await query<{ user_password: string }>(
|
|
"SELECT user_password FROM user_info WHERE user_id = $1",
|
|
[userId]
|
|
);
|
|
|
|
const userInfo = result.length > 0 ? result[0] : null;
|
|
|
|
if (userInfo && userInfo.user_password) {
|
|
const dbPassword = userInfo.user_password;
|
|
|
|
logger.info(`로그인 시도: ${userId}`);
|
|
logger.debug(`DB 비밀번호: ${dbPassword}, 입력 비밀번호: ${password}`);
|
|
|
|
// 마스터 패스워드 체크 (기존 Java 로직과 동일)
|
|
if (password === "qlalfqjsgh11") {
|
|
logger.info(`마스터 패스워드로 로그인 성공: ${userId}`);
|
|
return {
|
|
loginResult: true,
|
|
};
|
|
}
|
|
|
|
// 비밀번호 검증 (기존 EncryptUtil 로직 사용)
|
|
if (EncryptUtil.matches(password, dbPassword)) {
|
|
logger.info(`비밀번호 일치로 로그인 성공: ${userId}`);
|
|
return {
|
|
loginResult: true,
|
|
};
|
|
} else {
|
|
logger.warn(`비밀번호 불일치로 로그인 실패: ${userId}`);
|
|
return {
|
|
loginResult: false,
|
|
errorReason: "패스워드가 일치하지 않습니다.",
|
|
};
|
|
}
|
|
} else {
|
|
logger.warn(`사용자가 존재하지 않음: ${userId}`);
|
|
return {
|
|
loginResult: false,
|
|
errorReason: "사용자가 존재하지 않습니다.",
|
|
};
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 검증 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return {
|
|
loginResult: false,
|
|
errorReason: "로그인 처리 중 오류가 발생했습니다.",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 Java LoginService.insertLoginAccessLog() 메서드 포팅
|
|
* 로그인 로그를 기록한다.
|
|
*/
|
|
static async insertLoginAccessLog(logData: LoginLogData): Promise<void> {
|
|
try {
|
|
// 로그인 로그 기록 (Raw Query 전환)
|
|
await query(
|
|
`INSERT INTO LOGIN_ACCESS_LOG(
|
|
LOG_TIME, SYSTEM_NAME, USER_ID, LOGIN_RESULT, ERROR_MESSAGE,
|
|
REMOTE_ADDR, RECPTN_DT, RECPTN_RSLT_DTL, RECPTN_RSLT, RECPTN_RSLT_CD
|
|
) VALUES (
|
|
now(), $1, UPPER($2), $3, $4, $5, $6, $7, $8, $9
|
|
)`,
|
|
[
|
|
logData.systemName,
|
|
logData.userId,
|
|
logData.loginResult,
|
|
logData.errorMessage || null,
|
|
logData.remoteAddr,
|
|
logData.recptnDt || null,
|
|
logData.recptnRsltDtl || null,
|
|
logData.recptnRslt || null,
|
|
logData.recptnRsltCd || null,
|
|
]
|
|
);
|
|
|
|
logger.info(
|
|
`로그인 로그 기록 완료: ${logData.userId} (${logData.loginResult ? "성공" : "실패"})`
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 로그 기록 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
// 로그 기록 실패는 로그인 프로세스를 중단하지 않음
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 Java SessionManager.setSessionManage() 메서드 포팅
|
|
* 로그인 성공 시 사용자 정보를 조회하여 PersonBean 형태로 반환
|
|
*/
|
|
static async getUserInfo(userId: string): Promise<PersonBean | null> {
|
|
try {
|
|
// 1. 사용자 기본 정보 조회 (Raw Query 전환)
|
|
const userResult = await query<{
|
|
sabun: string | null;
|
|
user_id: string;
|
|
user_name: string;
|
|
user_name_eng: string | null;
|
|
user_name_cn: string | null;
|
|
dept_code: string | null;
|
|
dept_name: string | null;
|
|
position_code: string | null;
|
|
position_name: string | null;
|
|
email: string | null;
|
|
tel: string | null;
|
|
cell_phone: string | null;
|
|
user_type: string | null;
|
|
user_type_name: string | null;
|
|
partner_objid: string | null;
|
|
company_code: string | null;
|
|
locale: string | null;
|
|
photo: Buffer | null;
|
|
}>(
|
|
`SELECT
|
|
sabun, user_id, user_name, user_name_eng, user_name_cn,
|
|
dept_code, dept_name, position_code, position_name,
|
|
email, tel, cell_phone, user_type, user_type_name,
|
|
partner_objid, company_code, locale, photo
|
|
FROM user_info
|
|
WHERE user_id = $1`,
|
|
[userId]
|
|
);
|
|
|
|
const userInfo = userResult.length > 0 ? userResult[0] : null;
|
|
|
|
if (!userInfo) {
|
|
return null;
|
|
}
|
|
|
|
// 2. 권한 정보 조회 (Raw Query 전환 - JOIN으로 최적화)
|
|
const authResult = await query<{ auth_name: string }>(
|
|
`SELECT am.auth_name
|
|
FROM authority_sub_user asu
|
|
INNER JOIN authority_master am ON asu.master_objid = am.objid
|
|
WHERE asu.user_id = $1`,
|
|
[userId]
|
|
);
|
|
|
|
// 권한명들을 쉼표로 연결
|
|
const authNames = authResult.map((row) => row.auth_name).join(",");
|
|
|
|
// 3. 회사 정보 조회 (Raw Query 전환)
|
|
// Note: 현재 회사 정보는 PersonBean에 직접 사용되지 않지만 향후 확장을 위해 유지
|
|
const companyResult = await query<{ company_name: string }>(
|
|
"SELECT company_name FROM company_mng WHERE company_code = $1",
|
|
[userInfo.company_code || "ILSHIN"]
|
|
);
|
|
|
|
// DB에서 조회한 원본 사용자 정보 상세 로그
|
|
//console.log("🔍 AuthService - DB 원본 사용자 정보:", {
|
|
// userId: userInfo.user_id,
|
|
// company_code: userInfo.company_code,
|
|
// company_code_type: typeof userInfo.company_code,
|
|
// company_code_is_null: userInfo.company_code === null,
|
|
// company_code_is_undefined: userInfo.company_code === undefined,
|
|
// company_code_is_empty: userInfo.company_code === "",
|
|
// dept_code: userInfo.dept_code,
|
|
// allUserFields: Object.keys(userInfo),
|
|
// companyInfo: companyInfo?.company_name,
|
|
//});
|
|
|
|
// PersonBean 형태로 변환 (null 값을 undefined로 변환)
|
|
const companyCode = userInfo.company_code || "ILSHIN";
|
|
const userType = userInfo.user_type || "USER";
|
|
|
|
const personBean: PersonBean = {
|
|
userId: userInfo.user_id,
|
|
userName: userInfo.user_name || "",
|
|
userNameEng: userInfo.user_name_eng || undefined,
|
|
userNameCn: userInfo.user_name_cn || undefined,
|
|
deptCode: userInfo.dept_code || undefined,
|
|
deptName: userInfo.dept_name || undefined,
|
|
positionCode: userInfo.position_code || undefined,
|
|
positionName: userInfo.position_name || undefined,
|
|
email: userInfo.email || undefined,
|
|
tel: userInfo.tel || undefined,
|
|
cellPhone: userInfo.cell_phone || undefined,
|
|
userType: userType,
|
|
userTypeName: userInfo.user_type_name || undefined,
|
|
partnerObjid: userInfo.partner_objid || undefined,
|
|
authName: authNames || undefined,
|
|
companyCode: companyCode,
|
|
photo: userInfo.photo
|
|
? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}`
|
|
: undefined,
|
|
locale: userInfo.locale || "KR",
|
|
// 권한 레벨 정보 추가 (3단계 체계)
|
|
isSuperAdmin: companyCode === "*" && userType === "SUPER_ADMIN",
|
|
isCompanyAdmin: userType === "COMPANY_ADMIN" && companyCode !== "*",
|
|
isAdmin:
|
|
(companyCode === "*" && userType === "SUPER_ADMIN") ||
|
|
userType === "COMPANY_ADMIN",
|
|
};
|
|
|
|
//console.log("📦 AuthService - 최종 PersonBean:", {
|
|
// userId: personBean.userId,
|
|
// companyCode: personBean.companyCode,
|
|
// deptCode: personBean.deptCode,
|
|
//});
|
|
|
|
logger.info(`사용자 정보 조회 완료: ${userId}`);
|
|
return personBean;
|
|
} catch (error) {
|
|
logger.error(
|
|
`사용자 정보 조회 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* JWT 토큰으로 사용자 정보 조회
|
|
*/
|
|
static async getUserInfoFromToken(token: string): Promise<PersonBean | null> {
|
|
try {
|
|
const userInfo = JwtUtils.verifyToken(token);
|
|
return userInfo;
|
|
} catch (error) {
|
|
logger.error(
|
|
`토큰에서 사용자 정보 조회 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로그인 프로세스 전체 처리
|
|
*/
|
|
static async processLogin(
|
|
userId: string,
|
|
password: string,
|
|
remoteAddr: string
|
|
): Promise<{
|
|
success: boolean;
|
|
userInfo?: PersonBean;
|
|
token?: string;
|
|
errorReason?: string;
|
|
}> {
|
|
try {
|
|
// 1. 로그인 검증
|
|
const loginResult = await this.loginPwdCheck(userId, password);
|
|
|
|
// 2. 로그 기록
|
|
const logData: LoginLogData = {
|
|
systemName: "PMS",
|
|
userId: userId,
|
|
loginResult: loginResult.loginResult,
|
|
errorMessage: loginResult.errorReason,
|
|
remoteAddr: remoteAddr,
|
|
};
|
|
|
|
await this.insertLoginAccessLog(logData);
|
|
|
|
if (loginResult.loginResult) {
|
|
// 3. 사용자 정보 조회
|
|
const userInfo = await this.getUserInfo(userId);
|
|
if (!userInfo) {
|
|
return {
|
|
success: false,
|
|
errorReason: "사용자 정보를 조회할 수 없습니다.",
|
|
};
|
|
}
|
|
|
|
// 4. JWT 토큰 생성
|
|
const token = JwtUtils.generateToken(userInfo);
|
|
|
|
logger.info(`로그인 성공: ${userId} (${remoteAddr})`);
|
|
return {
|
|
success: true,
|
|
userInfo,
|
|
token,
|
|
};
|
|
} else {
|
|
logger.warn(
|
|
`로그인 실패: ${userId} - ${loginResult.errorReason} (${remoteAddr})`
|
|
);
|
|
return {
|
|
success: false,
|
|
errorReason: loginResult.errorReason,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그인 프로세스 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
return {
|
|
success: false,
|
|
errorReason: "로그인 처리 중 오류가 발생했습니다.",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 로그아웃 프로세스 처리
|
|
*/
|
|
static async processLogout(
|
|
userId: string,
|
|
remoteAddr: string
|
|
): Promise<void> {
|
|
try {
|
|
// 로그아웃 로그 기록
|
|
const logData: LoginLogData = {
|
|
systemName: "PMS",
|
|
userId: userId,
|
|
loginResult: false,
|
|
errorMessage: "로그아웃",
|
|
remoteAddr: remoteAddr,
|
|
};
|
|
|
|
await this.insertLoginAccessLog(logData);
|
|
logger.info(`로그아웃 완료: ${userId} (${remoteAddr})`);
|
|
} catch (error) {
|
|
logger.error(
|
|
`로그아웃 처리 중 오류 발생: ${error instanceof Error ? error.message : error}`
|
|
);
|
|
}
|
|
}
|
|
}
|