summaryrefslogtreecommitdiff
path: root/lib/users/session/repository.ts
blob: be7a0b2b2550d856727c9054f5433d7cea9de542 (plain)
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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
// lib/session/repository.ts
import db from '@/db/db'
import { 
    loginSessions, 
  tempAuthSessions, 
  pageVisits,
  type NewLoginSession,
  type NewTempAuthSession,
  type NewPageVisit,
  type LoginSession
} from '@/db/schema'
import { eq, and, desc, lt } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'


// 성능 최적화를 위한 캐시
const sessionCache = new Map<string, { data: any; timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000 // 5분 캐시

export class SessionRepository {
  // 임시 인증 세션 관리 (기존 메모리 저장소 대체)
  static async createTempAuthSession(data: {
    userId: number
    email: string
    authMethod: string
    expiresAt: Date
  }): Promise<string> {
    const tempAuthKey = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
    
    try {
      await db.insert(tempAuthSessions).values({
        tempAuthKey,
        userId: data.userId,
        email: data.email,
        authMethod: data.authMethod,
        expiresAt: data.expiresAt,
      })
      
      return tempAuthKey
    } catch (error) {
      console.error('Failed to create temp auth session:', error)
      throw error
    }
  }

  static async getTempAuthSession(tempAuthKey: string) {
    try {
      const result = await db
        .select()
        .from(tempAuthSessions)
        .where(
          and(
            eq(tempAuthSessions.tempAuthKey, tempAuthKey),
            eq(tempAuthSessions.isUsed, false)
          )
        )
        .limit(1)

      const session = result[0]
      if (!session || new Date() > session.expiresAt) {
        return null
      }

      return session
    } catch (error) {
      console.error('Failed to get temp auth session:', error)
      return null
    }
  }

  static async markTempAuthSessionAsUsed(tempAuthKey: string) {
    try {
      await db
        .update(tempAuthSessions)
        .set({ isUsed: true })
        .where(eq(tempAuthSessions.tempAuthKey, tempAuthKey))
    } catch (error) {
      console.error('Failed to mark temp auth session as used:', error)
    }
  }

  static async cleanupExpiredTempSessions() {
    try {
      await db
        .delete(tempAuthSessions)
        .where(lt(tempAuthSessions.expiresAt, new Date()))
    } catch (error) {
      console.error('Failed to cleanup expired temp sessions:', error)
    }
  }

  // 로그인 세션 관리
  static async createLoginSession(data: {
    userId: number
    ipAddress: string
    userAgent?: string
    authMethod: string
    sessionExpiredAt?: Date
    nextAuthSessionId?: string
  }): Promise<LoginSession> {
    try {
      const sessionData: NewLoginSession = {
        userId: data.userId,
        ipAddress: data.ipAddress,
        userAgent: data.userAgent,
        authMethod: data.authMethod,
        sessionExpiredAt: data.sessionExpiredAt,
        nextAuthSessionId: data.nextAuthSessionId,
      }

      const result = await db.insert(loginSessions).values(sessionData).returning()
      
      // 캐시에서 해당 사용자의 활성 세션 정보 제거
      sessionCache.delete(`active_session_${data.userId}`)
      
      return result[0]
    } catch (error) {
      console.error('Failed to create login session:', error)
      throw error
    }
  }

  static async updateLoginSession(sessionId: string, updates: {
    lastActivityAt?: Date
    sessionExpiredAt?: Date
    logoutAt?: Date
    isActive?: boolean
  }) {
    try {
      await db
        .update(loginSessions)
        .set({
          ...updates,
          updatedAt: new Date()
        })
        .where(eq(loginSessions.id, sessionId))
      
      // 캐시 무효화 (세션이 업데이트되었으므로)
      for (const [key] of sessionCache) {
        if (key.includes(sessionId)) {
          sessionCache.delete(key)
        }
      }
    } catch (error) {
      console.error('Failed to update login session:', error)
    }
  }

  // 캐시를 활용한 활성 세션 조회
  static async getActiveSessionByUserId(userId: number) {
    const cacheKey = `active_session_${userId}`
    const cached = sessionCache.get(cacheKey)
    
    // 캐시가 유효한 경우 반환
    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
      return cached.data
    }

    try {
      const result = await db
        .select()
        .from(loginSessions)
        .where(
          and(
            eq(loginSessions.userId, userId),
            eq(loginSessions.isActive, true)
          )
        )
        .orderBy(desc(loginSessions.loginAt))
        .limit(1)

      const session = result[0] || null
      
      // 캐시에 저장
      sessionCache.set(cacheKey, {
        data: session,
        timestamp: Date.now()
      })
      
      return session
    } catch (error) {
      console.error('Failed to get active session:', error)
      return null
    }
  }

  static async logoutSession(sessionId: string) {
    try {
      await db
        .update(loginSessions)
        .set({
          logoutAt: new Date(),
          isActive: false,
          updatedAt: new Date()
        })
        .where(eq(loginSessions.id, sessionId))
      
      // 캐시에서 관련된 세션 정보 제거
      for (const [key] of sessionCache) {
        if (key.includes(sessionId)) {
          sessionCache.delete(key)
        }
      }
    } catch (error) {
      console.error('Failed to logout session:', error)
    }
  }

  static async logoutAllUserSessions(userId: number) {
    try {
      await db
        .update(loginSessions)
        .set({
          logoutAt: new Date(),
          isActive: false,
          updatedAt: new Date()
        })
        .where(
          and(
            eq(loginSessions.userId, userId),
            eq(loginSessions.isActive, true)
          )
        )
      
      // 해당 사용자의 캐시 제거
      sessionCache.delete(`active_session_${userId}`)
    } catch (error) {
      console.error('Failed to logout all user sessions:', error)
    }
  }

  // 배치 처리를 위한 페이지 방문 기록 (성능 최적화)
  private static visitQueue: NewPageVisit[] = []
  private static isProcessingQueue = false

  static async recordPageVisit(data: {
    userId?: number
    sessionId?: string
    route: string
    pageTitle?: string
    referrer?: string
    ipAddress: string
    userAgent?: string
    queryParams?: string
    deviceType?: string
    browserName?: string
    osName?: string
  }) {
    const visitData: NewPageVisit = {
      userId: data.userId,
      sessionId: data.sessionId,
      route: data.route,
      pageTitle: data.pageTitle,
      referrer: data.referrer,
      ipAddress: data.ipAddress,
      userAgent: data.userAgent,
      queryParams: data.queryParams,
      deviceType: data.deviceType,
      browserName: data.browserName,
      osName: data.osName,
    }

    // 큐에 추가
    this.visitQueue.push(visitData)
    
    // 큐가 20개 이상이거나 3초마다 배치 처리
    if (this.visitQueue.length >= 20 || !this.isProcessingQueue) {
      this.processVisitQueue()
    }
  }

  // 배치 처리로 성능 최적화
  private static async processVisitQueue() {
    if (this.isProcessingQueue || this.visitQueue.length === 0) {
      return
    }

    this.isProcessingQueue = true
    
    try {
      const batch = this.visitQueue.splice(0, 100) // 최대 100개씩 처리
      
      if (batch.length > 0) {
        await db.insert(pageVisits).values(batch)
      }
    } catch (error) {
      console.error('Failed to process visit queue:', error)
    } finally {
      this.isProcessingQueue = false
      
      // 더 처리할 데이터가 있다면 재귀 호출
      if (this.visitQueue.length > 0) {
        setTimeout(() => this.processVisitQueue(), 100)
      }
    }
  }

  // 3초마다 큐 처리 (백그라운드)
  static {
    if (typeof setInterval !== 'undefined') {
      setInterval(() => {
        this.processVisitQueue()
      }, 3000)
    }
  }

  // 세션 활동 업데이트 (논블로킹, 에러 무시)
  static updateSessionActivity(sessionId: string): Promise<void> {
    return new Promise((resolve) => {
      // 비동기로 실행하되 메인 플로우를 블로킹하지 않음
      setImmediate(async () => {
        try {
          await this.updateLoginSession(sessionId, {
            lastActivityAt: new Date()
          })
        } catch (error) {
          // 에러를 로그만 남기고 무시
          console.error('Failed to update session activity (non-blocking):', error)
        }
        resolve()
      })
    })
  }

  static async updatePageVisitDuration(visitId: string, duration: number) {
    try {
      await db
        .update(pageVisits)
        .set({ duration })
        .where(eq(pageVisits.id, visitId))
    } catch (error) {
      console.error('Failed to update page visit duration:', error)
    }
  }

  // 캐시 정리 (메모리 관리)
  static cleanupCache() {
    const now = Date.now()
    
    for (const [key, value] of sessionCache) {
      if (now - value.timestamp > CACHE_TTL) {
        sessionCache.delete(key)
      }
    }
  }

  // 모니터링을 위한 통계 정보 제공
  static getRepositoryStats() {
    return {
      cacheSize: sessionCache.size,
      queueSize: this.visitQueue?.length || 0,
      cacheTTL: CACHE_TTL,
      isProcessingQueue: this.isProcessingQueue
    }
  }

  // 캐시 크기 조회 (모니터링용)
  static getCacheSize(): number {
    return sessionCache.size
  }

  // 큐 크기 조회 (모니터링용)
  static getQueueSize(): number {
    return this.visitQueue?.length || 0
  }

  // 정기적인 캐시 정리 (10분마다)
  static {
    if (typeof setInterval !== 'undefined') {
      setInterval(() => {
        this.cleanupCache()
      }, 10 * 60 * 1000)
    }
  }
}

// 에러 처리를 위한 래퍼 함수들
export const safeSessionOperations = {
  async recordPageVisit(data: Parameters<typeof SessionRepository.recordPageVisit>[0]) {
    try {
      await SessionRepository.recordPageVisit(data)
    } catch (error) {
      console.error('Safe page visit recording failed:', error)
    }
  },

  async updateSessionActivity(sessionId: string) {
    try {
      await SessionRepository.updateSessionActivity(sessionId)
    } catch (error) {
      console.error('Safe session activity update failed:', error)
    }
  },

  async getActiveSession(userId: number) {
    try {
      return await SessionRepository.getActiveSessionByUserId(userId)
    } catch (error) {
      console.error('Safe get active session failed:', error)
      return null
    }
  }
}

// lib/session/monitoring.ts - 성능 모니터링 (수정된 버전)
export class SessionMonitoring {
  private static metrics = {
    pageVisitRecords: 0,
    sessionUpdates: 0,
    cacheHits: 0,
    cacheMisses: 0,
    errors: 0
  }

  static incrementMetric(metric: keyof typeof this.metrics) {
    this.metrics[metric]++
  }

  static getMetrics() {
    return { ...this.metrics }
  }

  static resetMetrics() {
    Object.keys(this.metrics).forEach(key => {
      this.metrics[key as keyof typeof this.metrics] = 0
    })
  }

  // 성능 통계 로깅 (수정된 버전)
  static logPerformanceStats() {
    const repoStats = SessionRepository.getRepositoryStats()
    
    console.log('Session Repository Performance:', {
      ...this.metrics,
      cacheHitRate: this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) * 100 || 0,
      ...repoStats // cacheSize, queueSize 등 포함
    })
  }

  // 상세 성능 리포트 생성
  static getDetailedPerformanceReport() {
    const repoStats = SessionRepository.getRepositoryStats()
    const totalRequests = this.metrics.cacheHits + this.metrics.cacheMisses
    
    return {
      metrics: this.getMetrics(),
      repository: repoStats,
      performance: {
        cacheHitRate: totalRequests > 0 ? (this.metrics.cacheHits / totalRequests) * 100 : 0,
        errorRate: this.metrics.pageVisitRecords > 0 ? (this.metrics.errors / this.metrics.pageVisitRecords) * 100 : 0,
        queueUtilization: repoStats.queueSize / 100 * 100, // 100이 최대 큐 크기라고 가정
      },
      status: {
        healthy: this.metrics.errors / Math.max(this.metrics.pageVisitRecords, 1) < 0.01, // 1% 미만 에러율
        cacheEfficient: totalRequests > 0 ? (this.metrics.cacheHits / totalRequests) > 0.8 : true, // 80% 이상 캐시 히트율
        queueManageable: repoStats.queueSize < 50 // 큐 크기가 50 미만
      }
    }
  }
}