Files
AriWeb-Suite/modules/webmaker/lib/passpolicy.js

52 lines
3.5 KiB
JavaScript

'use strict';
// ============================================================================
// 비밀번호 규칙 (v1.3.17)
//
// 서비스 계정(빌더 사용자·관리자)과 사이트 관리자 계정의 비밀번호 규칙을 한곳에서 정한다.
// 관리자 콘솔 › 설정 › "비밀번호 규칙" 에서 바꾸며(settings.passwordPolicy), 기본값은 1.3.16 까지의 규칙
// "6자 이상" 그대로다(기존 사용자·화면 동작이 바뀌지 않게). 규칙은 새로 정하는 비밀번호(가입·변경·재설정)에만 적용하고,
// 이미 쓰고 있는 비밀번호로 로그인하는 것은 막지 않는다.
// ============================================================================
const DEFAULT = { minLength: 6, maxLength: 256, requireLetter: false, requireDigit: false, requireSymbol: false, requireMixedCase: false, forbidId: false };
const httpError = (m, s = 400) => Object.assign(new Error(m), { status: s });
/** 지금 규칙 (설정 파일 값 + 기본값). settings 를 늦게 불러 순환 의존을 피한다 */
function current() {
let p = {}; try { p = require('./settings').load().passwordPolicy || {}; } catch (_) {}
return normalize(p);
}
/** 관리자 입력 정리: 최소 길이 4~64, 최대 길이 최소 길이 이상 256 이하, 나머지는 켜기/끄기 */
function normalize(p) {
p = p || {}; const on = (v) => v === true || v === 'true' || v === 'on' || v === 1 || v === '1';
const min = Math.round(Number(p.minLength)); const max = Math.round(Number(p.maxLength));
const out = { ...DEFAULT };
if (Number.isFinite(min)) { if (min < 4 || min > 64) throw httpError('비밀번호 최소 길이는 4~64 사이로 정하세요'); out.minLength = min; }
if (Number.isFinite(max)) { if (max < out.minLength || max > 256) throw httpError('비밀번호 최대 길이는 최소 길이 이상 256 이하로 정하세요'); out.maxLength = max; }
for (const k of ['requireLetter', 'requireDigit', 'requireSymbol', 'requireMixedCase', 'forbidId']) if (p[k] !== undefined) out[k] = on(p[k]);
return out;
}
/** 규칙 설명 문장 (오류 메시지·화면 안내) */
function describe(pol = current()) {
const parts = [`${pol.minLength}자 이상`];
if (pol.maxLength < 256) parts.push(`${pol.maxLength}자 이하`);
if (pol.requireLetter) parts.push('영문자 포함'); if (pol.requireMixedCase) parts.push('대문자·소문자 모두 포함'); if (pol.requireDigit) parts.push('숫자 포함'); if (pol.requireSymbol) parts.push('특수문자 포함'); if (pol.forbidId) parts.push('아이디를 포함하지 않음');
return parts.join(', ');
}
/**
* 새 비밀번호 검사. 맞지 않으면 어떤 규칙인지 적은 오류를 던진다.
* @param {string} pw
* @param {object} [ctx] { id: 아이디(forbidId 검사), label: 오류 문장 앞말('비밀번호'·'새 비밀번호') }
*/
function check(pw, ctx = {}) {
const pol = current(); const s = String(pw || ''); const label = ctx.label || '비밀번호';
const fail = () => { throw httpError(`${label}는 ${describe(pol)} 이어야 합니다`); };
if (s.length < pol.minLength || s.length > pol.maxLength) fail();
if (pol.requireLetter && !/[A-Za-z]/.test(s)) fail();
if (pol.requireMixedCase && !(/[a-z]/.test(s) && /[A-Z]/.test(s))) fail();
if (pol.requireDigit && !/\d/.test(s)) fail();
if (pol.requireSymbol && !/[^A-Za-z0-9]/.test(s)) fail();
if (pol.forbidId && ctx.id && s.toLowerCase().includes(String(ctx.id).toLowerCase())) fail();
return true;
}
module.exports = { DEFAULT, current, normalize, describe, check };