返回 AiToEarn
request-context.interceptor.ts
根目录 / project / aitoearn-backend / libs / common / src / interceptors / request-context.interceptor.ts
1 import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'
2 import type { Observable } from 'rxjs'
3 import type { Locale } from '../i18n/messages'
4 import { AsyncLocalStorage } from 'node:async_hooks'
5 import { Injectable, UnauthorizedException } from '@nestjs/common'
6 import acceptLanguageParser from 'accept-language-parser'
7
8 export interface TokenInfo {
9 readonly id: string
10 readonly mail?: string
11 readonly name?: string
12 readonly shopDomain?: string
13 readonly exp?: number
14 }
15
16 interface RequestContextStore {
17 locale: Locale
18 user?: TokenInfo
19 }
20
21 export const requestContext = new AsyncLocalStorage<RequestContextStore>()
22
23 const SUPPORTED_LANGUAGES: Locale[] = ['en-US', 'zh-CN']
24
25 export function getLocale(): Locale {
26 return requestContext.getStore()?.locale || 'en-US'
27 }
28
29 export function getRequestContext(): RequestContextStore | undefined {
30 return requestContext.getStore()
31 }
32
33 /**
34 * Get authenticated user from request context.
35 * Throws UnauthorizedException if user is not authenticated.
36 * Use this for protected endpoints that require authentication.
37 */
38 export function getUser(): TokenInfo {
39 const user = requestContext.getStore()?.user
40 if (!user) {
41 throw new UnauthorizedException()
42 }
43 return user
44 }
45
46 /**
47 * Get authenticated user from request context, or undefined if not authenticated.
48 * Does not throw. Use this for public endpoints that optionally use user info.
49 */
50 export function getUserOptional(): TokenInfo | undefined {
51 return requestContext.getStore()?.user
52 }
53
54 @Injectable()
55 export class RequestContextInterceptor implements NestInterceptor {
56 public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
57 const locale = this.parseLocale(context)
58 const user = this.extractUser(context)
59 return requestContext.run({ locale, user }, () => next.handle())
60 }
61
62 private parseLocale(context: ExecutionContext): Locale {
63 const type = context.getType()
64
65 if (type === 'http')
66 return this.parseHttpLocale(context)
67
68 if (type === 'ws')
69 return this.parseWsLocale(context)
70
71 return 'en-US'
72 }
73
74 private extractUser(context: ExecutionContext): TokenInfo | undefined {
75 const type = context.getType()
76
77 if (type === 'http') {
78 const request = context.switchToHttp().getRequest()
79 return request['user']
80 }
81
82 return undefined
83 }
84
85 private parseHttpLocale(context: ExecutionContext): Locale {
86 const request = context.switchToHttp().getRequest()
87 const acceptLanguage = request.headers['accept-language']
88 return this.matchLocale(acceptLanguage)
89 }
90
91 private parseWsLocale(context: ExecutionContext): Locale {
92 const socket = context.switchToWs().getClient()
93 const acceptLanguage = socket.handshake?.headers?.['accept-language']
94 return this.matchLocale(acceptLanguage)
95 }
96
97 private matchLocale(acceptLanguage: string | undefined): Locale {
98 if (!acceptLanguage)
99 return 'en-US'
100
101 const matched = acceptLanguageParser.pick(SUPPORTED_LANGUAGES, acceptLanguage, { loose: true })
102 return (matched as Locale) || 'en-US'
103 }
104 }
105
105 lines TYPESCRIPT