feat: TOTP 2차 인증, 관리자 메뉴/대시보드 및 의존성 반영

- robthree/twofactorauth, Auth 설정·TotpService·2FA 뷰·라우트
- member TOTP 컬럼 DDL(login_tables, member_add_totp.sql)
- 관리자 메뉴·레이아웃·필터·대시보드 등 연관 변경
- env 샘플에 auth.requireTotp 주석

Made-with: Cursor
This commit is contained in:
taekyoungc
2026-03-26 15:29:55 +09:00
parent d36217920f
commit a3f92cd322
32 changed files with 1416 additions and 66 deletions

52
app/Config/Auth.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* 로그인·2차 인증(TOTP) 관련 설정
*
* .env 예:
* auth.requireTotp = true
* auth.totpIssuer = "쓰레기봉투 물류시스템"
*/
class Auth extends BaseConfig
{
/** 운영·스테이징 true 권장. 로컬 개발 시 false 로 1단계만 로그인 가능 */
public bool $requireTotp = true;
/** 인증 앱에 표시되는 발급자(issuer) */
public string $totpIssuer = '쓰레기봉투 물류시스템';
/** TOTP 연속 실패 시 세션 종료 전 허용 횟수 */
public int $totpMaxAttempts = 5;
/** 비밀번호 통과 후 2단계 완료까지 허용 시간(초) */
public int $pending2faTtlSeconds = 600;
public function __construct()
{
parent::__construct();
$require = env('auth.requireTotp');
if ($require !== null && $require !== '') {
$this->requireTotp = filter_var($require, FILTER_VALIDATE_BOOLEAN);
}
$issuer = env('auth.totpIssuer');
if (is_string($issuer) && $issuer !== '') {
$this->totpIssuer = $issuer;
}
$max = env('auth.totpMaxAttempts');
if ($max !== null && $max !== '' && is_numeric($max)) {
$this->totpMaxAttempts = max(1, (int) $max);
}
$ttl = env('auth.pending2faTtlSeconds');
if ($ttl !== null && $ttl !== '' && is_numeric($ttl)) {
$this->pending2faTtlSeconds = max(60, (int) $ttl);
}
}
}

View File

@@ -15,6 +15,8 @@ class Roles extends BaseConfig
* mb_level 상수 (member.mb_level)
*/
public const LEVEL_SUPER_ADMIN = 4;
/** 본부 관리자 — 현재는 super admin과 동일한 관리자 권한(지자체 선택 후 작업). 추후 super 전용 기능 분리 시 여기만 조정 */
public const LEVEL_HEADQUARTERS_ADMIN = 5;
public const LEVEL_LOCAL_ADMIN = 3; // 지자체관리자
public const LEVEL_SHOP = 2; // 지정판매소
public const LEVEL_CITIZEN = 1; // 일반 사용자(시민)
@@ -29,8 +31,27 @@ class Roles extends BaseConfig
self::LEVEL_SHOP => '지정판매소',
self::LEVEL_LOCAL_ADMIN => '지자체관리자',
self::LEVEL_SUPER_ADMIN => 'super admin',
self::LEVEL_HEADQUARTERS_ADMIN => '본부 관리자',
];
/**
* super admin(4) 또는 본부 관리자(5) — 동일 관리자 UX(지자체 선택 등)에 사용
*/
public static function isSuperAdminEquivalent(int $level): bool
{
return $level === self::LEVEL_SUPER_ADMIN || $level === self::LEVEL_HEADQUARTERS_ADMIN;
}
/**
* TOTP 2차 인증 적용 대상 (지자체·super·본부 관리자)
*/
public static function requiresTotp(int $level): bool
{
return $level === self::LEVEL_LOCAL_ADMIN
|| $level === self::LEVEL_SUPER_ADMIN
|| $level === self::LEVEL_HEADQUARTERS_ADMIN;
}
/**
* 자체 회원가입 시 기본 역할 (mb_level)
*/

View File

@@ -11,6 +11,7 @@ $routes->get('dashboard/classic-mock', 'Home::dashboardClassicMock');
$routes->get('dashboard/modern', 'Home::dashboardModern');
$routes->get('dashboard/dense', 'Home::dashboardDense');
$routes->get('dashboard/charts', 'Home::dashboardCharts');
$routes->get('dashboard/blend', 'Home::dashboardBlend');
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
@@ -29,6 +30,10 @@ $routes->get('bag/help', 'Bag::help');
// Auth
$routes->get('login', 'Auth::showLoginForm');
$routes->post('login', 'Auth::login');
$routes->get('login/two-factor', 'Auth::showTwoFactor');
$routes->post('login/two-factor', 'Auth::verifyTwoFactor');
$routes->get('login/totp-setup', 'Auth::showTotpSetup');
$routes->post('login/totp-setup', 'Auth::completeTotpSetup');
$routes->get('logout', 'Auth::logout');
$routes->get('register', 'Auth::showRegisterForm');
$routes->post('register', 'Auth::register');