teqo/src/utilities/campaignAuth.ts
2026-08-01 06:10:33 +00:00

194 lines
5.8 KiB
TypeScript

import 'server-only'
import type { CampaignUser } from '@/payload-types'
import configPromise from '@payload-config'
import { cookies } from 'next/headers'
import { createLocalReq, getPayload, jwtSign, logoutOperation, type Payload } from 'payload'
import { cache } from 'react'
import { CAMPAIGN_SESSION_TTL_LONG, CAMPAIGN_SESSION_TTL_SHORT } from '@/lib/campaignSessionTtl'
export const CAMPAIGN_TOKEN_COOKIE = 'campaign-token'
const CAMPAIGN_COOKIE_PATH = '/campanha'
type CampaignAuthPayload = Pick<Payload, 'auth' | 'findByID'>
type CampaignCookiePayload = Pick<Payload, 'secret'>
export type AuthenticatedCampaignUser = CampaignUser & { email: string }
type CampaignTokenClaims = Record<string, unknown> & {
collection: 'campaignUser'
id: number | string
sid: string
}
const campaignTokenClaims = (token: string): CampaignTokenClaims => {
try {
const segments = token.split('.')
if (segments.length !== 3 || !segments[1]) throw new Error('Malformed JWT')
const base64 = segments[1].replace(/-/g, '+').replace(/_/g, '/')
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')
const decoded = JSON.parse(atob(paddedBase64)) as unknown
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) {
throw new Error('Invalid campaign claims')
}
const claims = decoded as CampaignTokenClaims
if (
(typeof claims.id !== 'number' && typeof claims.id !== 'string') ||
claims.collection !== 'campaignUser' ||
typeof claims.sid !== 'string'
) {
throw new Error('Invalid campaign claims')
}
const { exp: _exp, iat: _iat, ...fieldsToSign } = claims
return fieldsToSign
} catch {
throw new Error('Token de campanha inválido.')
}
}
const campaignSessionToken = async (
token: string,
payload: CampaignCookiePayload,
tokenExpiration: number,
): Promise<string> => {
if (tokenExpiration === CAMPAIGN_SESSION_TTL_LONG) return token
const { token: resignedToken } = await jwtSign({
fieldsToSign: campaignTokenClaims(token),
secret: payload.secret,
tokenExpiration,
})
return resignedToken
}
export const revokeCampaignSession = async (token: string, payload: Payload): Promise<void> => {
const headers = new Headers({ Authorization: `JWT ${token}` })
const { user: authenticatedUser } = await payload.auth({ headers })
if (authenticatedUser?.collection !== 'campaignUser') {
throw new Error('Token de campanha inválido.')
}
const req = await createLocalReq(
{
req: { headers },
user: authenticatedUser,
},
payload,
)
await logoutOperation({
collection: payload.collections.campaignUser,
req,
})
}
export const authenticateCampaignToken = async (
token: string,
payload: CampaignAuthPayload,
): Promise<AuthenticatedCampaignUser | null> => {
try {
const headers = new Headers({ Authorization: `JWT ${token}` })
const { user } = await payload.auth({ headers })
if (user?.collection !== 'campaignUser') return null
const currentUser = await payload.findByID({
collection: 'campaignUser',
id: user.id,
depth: 0,
})
return {
...currentUser,
email: currentUser.email ?? '',
}
} catch {
return null
}
}
const readAuthenticatedCampaignUser = async (
depth: 0 | 1,
): Promise<AuthenticatedCampaignUser | null> => {
const cookieStore = await cookies()
const token = cookieStore.get(CAMPAIGN_TOKEN_COOKIE)?.value
if (!token) return null
const payload = await getPayload({ config: configPromise })
const user = await authenticateCampaignToken(token, payload)
if (!user) return null
if (depth === 0) return user
const withAvatar = await payload.findByID({
collection: 'campaignUser',
id: user.id,
depth: 1,
})
return {
...withAvatar,
email: withAvatar.email ?? '',
}
}
export const getCampaignUserRaw = (): Promise<AuthenticatedCampaignUser | null> =>
readAuthenticatedCampaignUser(0)
export const getCampaignUser = cache(getCampaignUserRaw)
export const getCampaignUserWithAvatar = cache(
(): Promise<AuthenticatedCampaignUser | null> => readAuthenticatedCampaignUser(1),
)
/** Public auth routes — session probe without reloading the user document. */
export const hasCampaignSession = cache(async (): Promise<boolean> => {
const cookieStore = await cookies()
const token = cookieStore.get(CAMPAIGN_TOKEN_COOKIE)?.value
if (!token) return false
try {
const payload = await getPayload({ config: configPromise })
const headers = new Headers({ Authorization: `JWT ${token}` })
const { user } = await payload.auth({ headers })
return user?.collection === 'campaignUser'
} catch {
return false
}
})
const campaignCookieOptions = (maxAge: number) => ({
httpOnly: true,
maxAge,
path: CAMPAIGN_COOKIE_PATH,
sameSite: 'lax' as const,
secure: process.env.NODE_ENV === 'production',
})
export const setCampaignAuthCookie = async (
token: string,
payload: CampaignCookiePayload,
tokenExpiration: number = CAMPAIGN_SESSION_TTL_SHORT,
): Promise<void> => {
const sessionToken = await campaignSessionToken(token, payload, tokenExpiration)
const cookieStore = await cookies()
cookieStore.set(CAMPAIGN_TOKEN_COOKIE, sessionToken, campaignCookieOptions(tokenExpiration))
}
export const clearCampaignAuthCookie = async (): Promise<void> => {
const cookieStore = await cookies()
cookieStore.set(CAMPAIGN_TOKEN_COOKIE, '', campaignCookieOptions(0))
}
export const campaignLoginCredentials = (
user: { email?: string | null; username?: string | null },
password: string,
): { email: string; password: string } | { username: string; password: string } | null => {
if (user.email) return { email: user.email, password }
if (user.username) return { username: user.username, password }
return null
}