# All files / middlewares auth.js

0% Statements 0/85
0% Branches 0/51
0% Functions 0/7
0% Lines 0/84

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356

     /**
     * 통합 인증/인가 미들웨어
     *
     * JWT 토큰 검증 및 권한 체크를 위한 미들웨어 모음
     *
     * @author TK-FB-Project
     * @since 2025-12-11
     */

    const jwt = require('jsonwebtoken');
    const { AuthenticationError, ForbiddenError } = require('../utils/errors');
    const logger = require('../utils/logger');

    /**
     * 권한 레벨 계층 구조
     * 숫자가 높을수록 상위 권한
     */
    const ACCESS_LEVELS = {
      worker: 1,
      group_leader: 2,
      support_team: 3,
      admin: 4,
      system: 5
    };

    /**
     * JWT 토큰 검증 미들웨어
     *
     * Authorization 헤더에서 Bearer 토큰을 추출하고 검증합니다.
     * 검증 성공 시 req.user에 디코딩된 사용자 정보를 저장합니다.
     *
     * @throws {AuthenticationError} 토큰이 없거나 유효하지 않을 때
     *
     * @example
     * router.get('/profile', requireAuth, getProfile);
     */
    const requireAuth = (req, res, next) => {
      try {
        const authHeader = req.headers['authorization'];

        if (!authHeader) {
          logger.warn('인증 실패: Authorization 헤더 없음', {
            path: req.path,
            method: req.method,
            ip: req.ip
          });
          throw new AuthenticationError('Authorization 헤더가 필요합니다');
        }

        const token = authHeader.split(' ')[1];

        if (!token) {
          logger.warn('인증 실패: Bearer 토큰 누락', {
            path: req.path,
            method: req.method,
            ip: req.ip
          });
          throw new AuthenticationError('Bearer 토큰이 필요합니다');
        }

        // JWT 검증
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;

        logger.debug('인증 성공', {
          user_id: decoded.user_id || decoded.id,
          username: decoded.username,
          role: decoded.role,
          access_level: decoded.access_level
        });

        next();
      } catch (err) {
        if (err.name === 'JsonWebTokenError') {
          logger.warn('인증 실패: 유효하지 않은 토큰', {
            error: err.message,
            path: req.path,
            ip: req.ip
          });
          throw new AuthenticationError('유효하지 않은 토큰입니다');
        } else if (err.name === 'TokenExpiredError') {
          logger.warn('인증 실패: 만료된 토큰', {
            error: err.message,
            path: req.path,
            ip: req.ip
          });
          throw new AuthenticationError('토큰이 만료되었습니다');
        } else if (err instanceof AuthenticationError) {
          // 이미 AuthenticationError인 경우 그대로 throw
          throw err;
        } else {
          logger.error('인증 처리 중 예상치 못한 오류', {
            error: err.message,
            stack: err.stack
          });
          throw new AuthenticationError('인증 처리 중 오류가 발생했습니다');
        }
      }
    };

    /**
     * 특정 역할(들) 권한 체크 미들웨어
     *
     * 사용자가 지정된 역할 중 하나를 가지고 있는지 확인합니다.
     * requireAuth 미들웨어가 먼저 실행되어야 합니다.
     *
     * @param {...string} roles - 허용할 역할 목록
     * @returns {Function} Express 미들웨어 함수
     * @throws {AuthenticationError} 인증되지 않은 경우
     * @throws {ForbiddenError} 권한이 없는 경우
     *
     * @example
     * // 단일 역할
     * router.post('/admin/users', requireAuth, requireRole('admin'), createUser);
     *
     * // 여러 역할
     * router.get('/reports', requireAuth, requireRole('admin', 'support_team'), getReports);
     */
    const requireRole = (...roles) => {
      return (req, res, next) => {
        try {
          if (!req.user) {
            logger.warn('권한 체크 실패: 인증되지 않은 요청', {
              path: req.path,
              method: req.method,
              ip: req.ip
            });
            throw new AuthenticationError('인증이 필요합니다');
          }

          const userRole = req.user.role;

          if (!roles.includes(userRole)) {
            logger.warn('권한 체크 실패: 역할 불일치', {
              user_id: req.user.user_id || req.user.id,
              username: req.user.username,
              current_role: userRole,
              required_roles: roles,
              path: req.path
            });
            throw new ForbiddenError(
              `이 기능을 사용하려면 ${roles.join(' 또는 ')} 권한이 필요합니다`
            );
          }

          logger.debug('역할 권한 확인 성공', {
            user_id: req.user.user_id || req.user.id,
            username: req.user.username,
            role: userRole,
            required_roles: roles
          });

          next();
        } catch (err) {
          next(err);
        }
      };
    };

    /**
     * 최소 권한 레벨 체크 미들웨어 (계층적)
     *
     * 사용자가 요구되는 최소 권한 레벨 이상인지 확인합니다.
     * worker(1) < group_leader(2) < support_team(3) < admin(4) < system(5)
     * requireAuth 미들웨어가 먼저 실행되어야 합니다.
     *
     * @param {string} minLevel - 최소 권한 레벨 (worker, group_leader, support_team, admin, system)
     * @returns {Function} Express 미들웨어 함수
     * @throws {AuthenticationError} 인증되지 않은 경우
     * @throws {ForbiddenError} 권한이 부족한 경우
     *
     * @example
     * // admin 이상 필요 (admin, system만 허용)
     * router.delete('/users/:id', requireAuth, requireMinLevel('admin'), deleteUser);
     *
     * // group_leader 이상 필요 (group_leader, support_team, admin, system 허용)
     * router.get('/team-reports', requireAuth, requireMinLevel('group_leader'), getTeamReports);
     */
    const requireMinLevel = (minLevel) => {
      return (req, res, next) => {
        try {
          if (!req.user) {
            logger.warn('권한 레벨 체크 실패: 인증되지 않은 요청', {
              path: req.path,
              method: req.method,
              ip: req.ip
            });
            throw new AuthenticationError('인증이 필요합니다');
          }

          const userLevel = ACCESS_LEVELS[req.user.access_level] || 0;
          const requiredLevel = ACCESS_LEVELS[minLevel] || 999;

          if (userLevel < requiredLevel) {
            logger.warn('권한 레벨 체크 실패: 권한 부족', {
              user_id: req.user.user_id || req.user.id,
              username: req.user.username,
              current_level: req.user.access_level,
              current_level_value: userLevel,
              required_level: minLevel,
              required_level_value: requiredLevel,
              path: req.path
            });
            throw new ForbiddenError(
              `이 기능을 사용하려면 ${minLevel} 이상의 권한이 필요합니다 (현재: ${req.user.access_level})`
            );
          }

          logger.debug('권한 레벨 확인 성공', {
            user_id: req.user.user_id || req.user.id,
            username: req.user.username,
            access_level: req.user.access_level,
            required_level: minLevel
          });

          next();
        } catch (err) {
          next(err);
        }
      };
    };

    /**
     * 리소스 소유자 또는 관리자 권한 체크 미들웨어
     *
     * 요청한 사용자가 리소스의 소유자이거나 관리자 권한이 있는지 확인합니다.
     * requireAuth 미들웨어가 먼저 실행되어야 합니다.
     *
     * @param {Object} options - 옵션 객체
     * @param {string} options.resourceField - 리소스 ID를 가져올 req 필드 (예: 'params.user_id', 'body.worker_id')
     * @param {string} options.userField - 사용자 ID 필드명 (기본값: 'user_id', 'id'도 자동 시도)
     * @param {string[]} options.adminRoles - 관리자로 인정할 역할들 (기본값: ['admin', 'system'])
     * @returns {Function} Express 미들웨어 함수
     * @throws {AuthenticationError} 인증되지 않은 경우
     * @throws {ForbiddenError} 소유자도 아니고 관리자도 아닌 경우
     *
     * @example
     * // URL 파라미터의 user_id로 체크
     * router.put('/users/:user_id', requireAuth, requireOwnerOrAdmin({
     *   resourceField: 'params.user_id'
     * }), updateUser);
     *
     * // 요청 body의 worker_id로 체크, support_team도 관리자로 인정
     * router.delete('/reports/:id', requireAuth, requireOwnerOrAdmin({
     *   resourceField: 'body.worker_id',
     *   adminRoles: ['admin', 'system', 'support_team']
     * }), deleteReport);
     */
    const requireOwnerOrAdmin = (options = {}) => {
      const {
        resourceField = 'params.id',
        userField = 'user_id',
        adminRoles = ['admin', 'system']
      } = options;

      return (req, res, next) => {
        try {
          if (!req.user) {
            logger.warn('소유자/관리자 체크 실패: 인증되지 않은 요청', {
              path: req.path,
              method: req.method,
              ip: req.ip
            });
            throw new AuthenticationError('인증이 필요합니다');
          }

          // 관리자 권한 체크
          const userRole = req.user.role;
          const isAdmin = adminRoles.includes(userRole);

          if (isAdmin) {
            logger.debug('관리자 권한으로 접근 허용', {
              user_id: req.user.user_id || req.user.id,
              username: req.user.username,
              role: userRole,
              path: req.path
            });
            return next();
          }

          // 리소스 ID 추출
          const fieldParts = resourceField.split('.');
          let resourceId = req;
          for (const part of fieldParts) {
            resourceId = resourceId[part];
            if (resourceId === undefined) break;
          }

          // 사용자 ID (user_id 또는 id)
          const userId = req.user[userField] || req.user.id || req.user.user_id;

          // 소유자 체크
          const isOwner = resourceId && String(resourceId) === String(userId);

          if (!isOwner) {
            logger.warn('소유자/관리자 체크 실패: 권한 부족', {
              user_id: userId,
              username: req.user.username,
              role: userRole,
              resource_id: resourceId,
              resource_field: resourceField,
              is_admin: isAdmin,
              is_owner: isOwner,
              path: req.path
            });
            throw new ForbiddenError('본인의 리소스이거나 관리자 권한이 필요합니다');
          }

          logger.debug('리소스 소유자로 접근 허용', {
            user_id: userId,
            username: req.user.username,
            resource_id: resourceId,
            path: req.path
          });

          next();
        } catch (err) {
          next(err);
        }
      };
    };

    /**
     * 레거시 호환성을 위한 별칭
     * @deprecated requireAuth를 사용하세요
     */
    const verifyToken = requireAuth;

    /**
     * 레거시 호환성을 위한 별칭
     * @deprecated requireRole('admin', 'system')을 사용하세요
     */
    const requireAdmin = requireRole('admin', 'system');

    /**
     * 레거시 호환성을 위한 별칭
     * @deprecated requireRole('system')을 사용하세요
     */
    const requireSystem = requireRole('system');

    module.exports = {
      // 주요 미들웨어
      requireAuth,
      requireRole,
      requireMinLevel,
      requireOwnerOrAdmin,

      // 레거시 호환성
      verifyToken,
      requireAdmin,
      requireSystem,

      // 상수
      ACCESS_LEVELS
    };