refac: gera url de reset de senha com base no .env
This commit is contained in:
@@ -15,3 +15,4 @@ JWT_PUBLIC_KEY_PEM="<replace-with-escaped-pem>"
|
|||||||
REFRESH_TOKEN_PEPPER="change-me"
|
REFRESH_TOKEN_PEPPER="change-me"
|
||||||
PASSWORD_RESET_TTL_SEC="900"
|
PASSWORD_RESET_TTL_SEC="900"
|
||||||
PASSWORD_RESET_TOKEN_PEPPER="change-me"
|
PASSWORD_RESET_TOKEN_PEPPER="change-me"
|
||||||
|
PASSWORD_RESET_BASE_URL="http://localhost:3000"
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@vee-validate/nuxt', 'vue-sonner/nuxt', '@nuxt/icon'],
|
modules: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'@vee-validate/nuxt',
|
||||||
|
'vue-sonner/nuxt',
|
||||||
|
'@nuxt/icon'
|
||||||
|
],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
databaseUrl: process.env.DATABASE_URL,
|
databaseUrl: process.env.DATABASE_URL,
|
||||||
jwtIssuer: process.env.JWT_ISSUER ?? 'https://auth.local',
|
jwtIssuer: process.env.JWT_ISSUER ?? 'https://auth.local',
|
||||||
@@ -14,17 +20,12 @@ export default defineNuxtConfig({
|
|||||||
jwtKid: process.env.JWT_KID ?? 'auth-key-1',
|
jwtKid: process.env.JWT_KID ?? 'auth-key-1',
|
||||||
refreshTokenPepper: process.env.REFRESH_TOKEN_PEPPER ?? '',
|
refreshTokenPepper: process.env.REFRESH_TOKEN_PEPPER ?? '',
|
||||||
passwordResetTtlSec: process.env.PASSWORD_RESET_TTL_SEC ?? '900',
|
passwordResetTtlSec: process.env.PASSWORD_RESET_TTL_SEC ?? '900',
|
||||||
passwordResetTokenPepper: process.env.PASSWORD_RESET_TOKEN_PEPPER ?? ''
|
passwordResetTokenPepper: process.env.PASSWORD_RESET_TOKEN_PEPPER ?? '',
|
||||||
|
passwordResetBaseUrl: process.env.PASSWORD_RESET_BASE_URL ?? ''
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: ['@vue/devtools-core', '@vue/devtools-kit', 'zod', '@vee-validate/zod', 'vue-sonner']
|
||||||
'@vue/devtools-core',
|
|
||||||
'@vue/devtools-kit',
|
|
||||||
'zod',
|
|
||||||
'@vee-validate/zod',
|
|
||||||
'vue-sonner',
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -11,6 +11,7 @@ export interface AuthRuntimeConfig {
|
|||||||
kid: string
|
kid: string
|
||||||
refreshTokenPepper: string
|
refreshTokenPepper: string
|
||||||
passwordResetTokenPepper: string
|
passwordResetTokenPepper: string
|
||||||
|
passwordResetBaseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,6 +86,7 @@ export function getAuthRuntimeConfig(event?: H3Event): AuthRuntimeConfig {
|
|||||||
const refreshTokenPepper = String(runtimeConfig.refreshTokenPepper ?? '').trim()
|
const refreshTokenPepper = String(runtimeConfig.refreshTokenPepper ?? '').trim()
|
||||||
const passwordResetTokenPepper =
|
const passwordResetTokenPepper =
|
||||||
String(runtimeConfig.passwordResetTokenPepper ?? '').trim() || refreshTokenPepper
|
String(runtimeConfig.passwordResetTokenPepper ?? '').trim() || refreshTokenPepper
|
||||||
|
const passwordResetBaseUrl = String(runtimeConfig.passwordResetBaseUrl ?? '').trim()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
issuer,
|
issuer,
|
||||||
@@ -99,6 +101,7 @@ export function getAuthRuntimeConfig(event?: H3Event): AuthRuntimeConfig {
|
|||||||
privateKeyPem: normalizePem(String(runtimeConfig.jwtPrivateKeyPem ?? ''), 'JWT private key'),
|
privateKeyPem: normalizePem(String(runtimeConfig.jwtPrivateKeyPem ?? ''), 'JWT private key'),
|
||||||
publicKeyPem: normalizePem(String(runtimeConfig.jwtPublicKeyPem ?? ''), 'JWT public key'),
|
publicKeyPem: normalizePem(String(runtimeConfig.jwtPublicKeyPem ?? ''), 'JWT public key'),
|
||||||
refreshTokenPepper,
|
refreshTokenPepper,
|
||||||
passwordResetTokenPepper
|
passwordResetTokenPepper,
|
||||||
|
passwordResetBaseUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import { createError, readBody, setResponseStatus, type H3Event } from 'h3'
|
import { createError, getRequestURL, readBody, setResponseStatus, type H3Event } from 'h3'
|
||||||
|
|
||||||
import { signAccessToken } from './jwt'
|
import { signAccessToken } from './jwt'
|
||||||
import { getAuthRuntimeConfig } from './auth-config'
|
import { getAuthRuntimeConfig } from './auth-config'
|
||||||
@@ -202,6 +202,7 @@ export async function handleForgotPassword(event: H3Event) {
|
|||||||
const expiresAt = new Date(now.getTime() + config.passwordResetTtlSec * 1000)
|
const expiresAt = new Date(now.getTime() + config.passwordResetTtlSec * 1000)
|
||||||
const rawResetToken = generateRawPasswordResetToken()
|
const rawResetToken = generateRawPasswordResetToken()
|
||||||
const tokenHash = hashPasswordResetToken(rawResetToken, config.passwordResetTokenPepper)
|
const tokenHash = hashPasswordResetToken(rawResetToken, config.passwordResetTokenPepper)
|
||||||
|
const resetBaseUrl = config.passwordResetBaseUrl || getRequestURL(event).origin
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email },
|
where: { email },
|
||||||
@@ -233,7 +234,7 @@ export async function handleForgotPassword(event: H3Event) {
|
|||||||
message: 'If the email exists, recovery instructions were generated',
|
message: 'If the email exists, recovery instructions were generated',
|
||||||
recovery: {
|
recovery: {
|
||||||
reset_token: rawResetToken,
|
reset_token: rawResetToken,
|
||||||
reset_url: buildPasswordResetPreviewUrl(config.issuer, rawResetToken),
|
reset_url: buildPasswordResetPreviewUrl(resetBaseUrl, rawResetToken),
|
||||||
expires_in: config.passwordResetTtlSec
|
expires_in: config.passwordResetTtlSec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ export function generateRawPasswordResetToken(): string {
|
|||||||
/**
|
/**
|
||||||
* Monta uma URL de preview para facilitar testes locais sem SMTP.
|
* Monta uma URL de preview para facilitar testes locais sem SMTP.
|
||||||
*
|
*
|
||||||
* @param issuer Base do serviço de auth.
|
* @param baseUrl URL pública usada para abrir a tela de redefinição.
|
||||||
* @param token Token bruto de recuperação.
|
* @param token Token bruto de recuperação.
|
||||||
* @returns URL completa (ou fallback relativo) com o token.
|
* @returns URL completa (ou fallback relativo) com o token.
|
||||||
*/
|
*/
|
||||||
export function buildPasswordResetPreviewUrl(issuer: string, token: string): string {
|
export function buildPasswordResetPreviewUrl(baseUrl: string, token: string): string {
|
||||||
try {
|
try {
|
||||||
const url = new URL('/auth/reset-password', issuer)
|
const url = new URL('/auth/reset-password', baseUrl)
|
||||||
url.searchParams.set('token', token)
|
url.searchParams.set('token', token)
|
||||||
|
|
||||||
return url.toString()
|
return url.toString()
|
||||||
|
|||||||
Reference in New Issue
Block a user