mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-09 12:27:59 -05:00
Password reset without keycloak UI
This commit is contained in:
@@ -7,7 +7,7 @@ import MobileNav from './MobileNav'
|
||||
import ThemeSwitch from './ThemeSwitch'
|
||||
import headerNavLinks from '../data/headerNavLinks'
|
||||
import Logo from './Logo'
|
||||
import { Dialog, DialogTrigger } from './ui/dialog'
|
||||
import { Dialog, DialogContent, DialogTrigger } from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import RegisterFormModal from './RegisterFormModal'
|
||||
import LoginFormModal from './LoginFormModal'
|
||||
@@ -62,17 +62,21 @@ const Header = () => {
|
||||
Login
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
<DialogContent>
|
||||
<LoginFormModal
|
||||
close={() => setLoginIsOpen(false)}
|
||||
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-24">Register</Button>
|
||||
</DialogTrigger>
|
||||
<RegisterFormModal close={() => setRegisterIsOpen(false)} />
|
||||
<DialogContent>
|
||||
<RegisterFormModal close={() => setRegisterIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
@@ -89,7 +93,9 @@ const Header = () => {
|
||||
</div>
|
||||
|
||||
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
<DialogContent>
|
||||
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ const LayoutWrapper = ({ children }: Props) => {
|
||||
className={`${inter.className} flex h-screen flex-col justify-between font-sans`}
|
||||
>
|
||||
<Header />
|
||||
<main className="mb-auto">{children}</main>
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
|
||||
@@ -79,7 +79,7 @@ function LoginFormModal({ close, openPasswordResetModal }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Login</DialogTitle>
|
||||
<DialogDescription>Log into your account.</DialogDescription>
|
||||
@@ -137,7 +137,7 @@ function LoginFormModal({ close, openPasswordResetModal }: Props) {
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ function PasswordResetFormModal({ close }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset Password</DialogTitle>
|
||||
<DialogDescription>Recover your account.</DialogDescription>
|
||||
@@ -86,7 +86,7 @@ function PasswordResetFormModal({ close }: Props) {
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,8 @@ import {
|
||||
FormMessage,
|
||||
} from './ui/form'
|
||||
import { Button } from './ui/button'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { trpc } from '../utils/trpc'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -56,7 +54,6 @@ function RegisterFormModal({ close }: Props) {
|
||||
})
|
||||
|
||||
close()
|
||||
form.reset({ email: '', password: '', confirmPassword: '' })
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
@@ -76,7 +73,7 @@ function RegisterFormModal({ close }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Register</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -139,7 +136,7 @@ function RegisterFormModal({ close }: Props) {
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import { faGithub, faTwitter } from "@fortawesome/free-brands-svg-icons"
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import Link from "next/link"
|
||||
import escapeHTML from "escape-html"
|
||||
import { ProjectItem } from "../utils/types"
|
||||
import { faGithub, faTwitter } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import escapeHTML from 'escape-html'
|
||||
import { ProjectItem } from '../utils/types'
|
||||
|
||||
const ShareButtons: React.FC<{ project: ProjectItem }> = ({ project }) => {
|
||||
const { git, twitter, website } = project;
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<Link href={escapeHTML(git)} passHref legacyBehavior>
|
||||
<a className="projectlist">
|
||||
<FontAwesomeIcon
|
||||
icon={faGithub}
|
||||
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`https://twitter.com/${escapeHTML(twitter)}`} passHref legacyBehavior>
|
||||
<a className="projectlist">
|
||||
<FontAwesomeIcon
|
||||
icon={faTwitter}
|
||||
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
{website && <Link href={escapeHTML(website)} passHref legacyBehavior>
|
||||
<a className="projectlist">
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</Link>}
|
||||
</div>
|
||||
)
|
||||
const { git, twitter, website } = project
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<Link href={escapeHTML(git)} passHref legacyBehavior>
|
||||
<a className="projectlist">
|
||||
<FontAwesomeIcon
|
||||
icon={faGithub}
|
||||
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href={`https://twitter.com/${escapeHTML(twitter)}`}
|
||||
passHref
|
||||
legacyBehavior
|
||||
>
|
||||
<a className="projectlist">
|
||||
<FontAwesomeIcon
|
||||
icon={faTwitter}
|
||||
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
{website && (
|
||||
<Link href={escapeHTML(website)} passHref legacyBehavior>
|
||||
<a className="projectlist">
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default ShareButtons
|
||||
export default ShareButtons
|
||||
|
||||
163
pages/reset-password/[token].tsx
Normal file
163
pages/reset-password/[token].tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { ReloadIcon } from '@radix-ui/react-icons'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useRouter } from 'next/router'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { z } from 'zod'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../../components/ui/form'
|
||||
import { Input } from '../../components/ui/input'
|
||||
import { Button } from '../../components/ui/button'
|
||||
import { toast } from '../../components/ui/use-toast'
|
||||
import { useEffect } from 'react'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match.',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
|
||||
type ResetPasswordFormInputs = z.infer<typeof schema>
|
||||
|
||||
function ResetPassword() {
|
||||
const router = useRouter()
|
||||
|
||||
const form = useForm<ResetPasswordFormInputs>({
|
||||
resolver: zodResolver(schema),
|
||||
})
|
||||
|
||||
const resetPasswordMutation = trpc.auth.resetPassword.useMutation()
|
||||
|
||||
async function onSubmit(data: ResetPasswordFormInputs) {
|
||||
const { token } = router.query
|
||||
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
await resetPasswordMutation.mutateAsync({
|
||||
token: token as string,
|
||||
password: data.password,
|
||||
})
|
||||
|
||||
toast({ title: 'Password successfully reset. You may now log in.' })
|
||||
router.push(`/?loginEmail=${data.email}`)
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
if (errorMessage === 'INVALID_TOKEN') {
|
||||
toast({
|
||||
title: 'Invalid password reset link.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
|
||||
router.push('/')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { token } = router.query
|
||||
|
||||
if (token) {
|
||||
const decoded = jwtDecode(token as string) as { email: string }
|
||||
|
||||
if (decoded.email) {
|
||||
form.setValue('email', decoded.email)
|
||||
}
|
||||
}
|
||||
}, [router.query.token])
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md m-auto">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<div className="flex flex-col space-y-1.5 text-center sm:text-left">
|
||||
<span className="text-lg font-semibold leading-none tracking-tight">
|
||||
Password Reset
|
||||
</span>
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Reset your password
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm password</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}{' '}
|
||||
Reset Password
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPassword
|
||||
@@ -13,7 +13,6 @@ function VerifyEmail() {
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
console.log(token)
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,40 +4,41 @@ import jwt from 'jsonwebtoken'
|
||||
|
||||
import { publicProcedure, router } from '../trpc'
|
||||
import { authenticateKeycloakClient } from '../utils/keycloak'
|
||||
import { keycloak, sendgrid, transporter } from '../services'
|
||||
import { keycloak, transporter } from '../services'
|
||||
import { env } from '../../env.mjs'
|
||||
|
||||
type EmailVerifyJwtPayload = {
|
||||
action: 'email_verify'
|
||||
userId: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type PasswordResetJwtPayload = {
|
||||
action: 'password-reset'
|
||||
userId: string
|
||||
email: string
|
||||
tokenVersion: number
|
||||
}
|
||||
|
||||
export const authRouter = router({
|
||||
register: publicProcedure
|
||||
.input(z.object({ email: z.string().email(), password: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
let user: { id: string }
|
||||
|
||||
try {
|
||||
const user = await keycloak.users.create({
|
||||
user = await keycloak.users.create({
|
||||
realm: 'monerofund',
|
||||
email: input.email,
|
||||
credentials: [
|
||||
{ type: 'password', value: input.password, temporary: false },
|
||||
],
|
||||
requiredActions: ['VERIFY_EMAIL'],
|
||||
attributes: { passwordResetTokenVersion: 1 },
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
// Send verification email with Sendgrid
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, email: input.email },
|
||||
env.NEXTAUTH_SECRET,
|
||||
{ expiresIn: '1d' }
|
||||
)
|
||||
|
||||
// no await here as we don't want to block the response
|
||||
transporter.sendMail({
|
||||
from: env.SENDGRID_VERIFIED_SENDER,
|
||||
to: input.email,
|
||||
subject: 'Verify your email',
|
||||
html: `<a href="${env.APP_URL}/verify-email/${token}" target="_blank">Verify email</a>`,
|
||||
})
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as any).responseData.errorMessage ===
|
||||
@@ -46,60 +47,175 @@ export const authRouter = router({
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'EMAIL_TAKEN' })
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'UNKNOWN_ERROR',
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
const payload: EmailVerifyJwtPayload = {
|
||||
action: 'email_verify',
|
||||
userId: user.id,
|
||||
email: input.email,
|
||||
}
|
||||
|
||||
const emailVerifyToken = jwt.sign(payload, env.NEXTAUTH_SECRET, {
|
||||
expiresIn: '1d',
|
||||
})
|
||||
|
||||
// no await here as we don't want to block the response
|
||||
transporter.sendMail({
|
||||
from: env.SENDGRID_VERIFIED_SENDER,
|
||||
to: input.email,
|
||||
subject: 'Verify your email',
|
||||
html: `<a href="${env.APP_URL}/verify-email/${emailVerifyToken}" target="_blank">Verify email</a>`,
|
||||
})
|
||||
}),
|
||||
|
||||
verifyEmail: publicProcedure
|
||||
.input(z.object({ token: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
let decoded: EmailVerifyJwtPayload
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(input.token, env.NEXTAUTH_SECRET) as {
|
||||
userId: string
|
||||
email: string
|
||||
}
|
||||
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
await keycloak.users.update(
|
||||
{ id: decoded.userId },
|
||||
{ emailVerified: true, requiredActions: [] }
|
||||
)
|
||||
|
||||
return { email: decoded.email }
|
||||
decoded = jwt.verify(
|
||||
input.token,
|
||||
env.NEXTAUTH_SECRET
|
||||
) as EmailVerifyJwtPayload
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'UNKNOWN_ERROR',
|
||||
code: 'FORBIDDEN',
|
||||
message: 'INVALID_TOKEN',
|
||||
})
|
||||
}
|
||||
|
||||
if (decoded.action !== 'email_verify') {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'INVALID_ACTION',
|
||||
})
|
||||
}
|
||||
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
await keycloak.users.update(
|
||||
{ id: decoded.userId },
|
||||
{ emailVerified: true, requiredActions: [] }
|
||||
)
|
||||
|
||||
return { email: decoded.email }
|
||||
}),
|
||||
|
||||
requestPasswordReset: publicProcedure
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await authenticateKeycloakClient()
|
||||
await authenticateKeycloakClient()
|
||||
const users = await keycloak.users.find({ email: input.email })
|
||||
const user = users[0]
|
||||
|
||||
const users = await keycloak.users.find({ email: input.email })
|
||||
const userId = users[0]?.id
|
||||
|
||||
if (!userId) return
|
||||
|
||||
await keycloak.users.executeActionsEmail({
|
||||
id: userId,
|
||||
actions: ['UPDATE_PASSWORD'],
|
||||
if (!user || !user.id || !user.attributes)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'USER_NOT_FOUND',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
const passwordResetTokenVersion =
|
||||
parseInt(user.attributes.passwordResetTokenVersion?.[0]) || null
|
||||
|
||||
if (!passwordResetTokenVersion) {
|
||||
console.error(
|
||||
`User ${user.id} has no passwordResetTokenVersion attribute`
|
||||
)
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'UNKNOWN_ERROR',
|
||||
})
|
||||
}
|
||||
|
||||
const payload: PasswordResetJwtPayload = {
|
||||
action: 'password-reset',
|
||||
userId: user.id,
|
||||
email: input.email,
|
||||
tokenVersion: passwordResetTokenVersion,
|
||||
}
|
||||
|
||||
const passwordResetToken = jwt.sign(payload, env.NEXTAUTH_SECRET, {
|
||||
expiresIn: '30m',
|
||||
})
|
||||
|
||||
// no await here as we don't want to block the response
|
||||
transporter.sendMail({
|
||||
from: env.SENDGRID_VERIFIED_SENDER,
|
||||
to: input.email,
|
||||
subject: 'Reset your password',
|
||||
html: `<a href="${env.APP_URL}/reset-password/${passwordResetToken}" target="_blank">Reset password</a>`,
|
||||
})
|
||||
}),
|
||||
|
||||
resetPassword: publicProcedure
|
||||
.input(z.object({ token: z.string(), password: z.string().min(8) }))
|
||||
.mutation(async ({ input }) => {
|
||||
let decoded: PasswordResetJwtPayload
|
||||
|
||||
try {
|
||||
decoded = jwt.verify(
|
||||
input.token,
|
||||
env.NEXTAUTH_SECRET
|
||||
) as PasswordResetJwtPayload
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'INVALID_TOKEN',
|
||||
})
|
||||
}
|
||||
|
||||
if (decoded.action !== 'password-reset') {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'INVALID_ACTION',
|
||||
})
|
||||
}
|
||||
|
||||
await authenticateKeycloakClient()
|
||||
const user = await keycloak.users.findOne({ id: decoded.userId })
|
||||
|
||||
if (!user || !user.attributes)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'USER_NOT_FOUND',
|
||||
})
|
||||
|
||||
const passwordResetTokenVersion =
|
||||
parseInt(user.attributes.passwordResetTokenVersion?.[0]) || null
|
||||
|
||||
if (!passwordResetTokenVersion) {
|
||||
console.error(
|
||||
`User ${user.id} has no passwordResetTokenVersion attribute`
|
||||
)
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
})
|
||||
}
|
||||
|
||||
if (decoded.tokenVersion !== passwordResetTokenVersion)
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'INVALID_TOKEN',
|
||||
})
|
||||
|
||||
await keycloak.users.update(
|
||||
{ id: decoded.userId },
|
||||
{
|
||||
email: decoded.email,
|
||||
credentials: [
|
||||
{ type: 'password', value: input.password, temporary: false },
|
||||
],
|
||||
attributes: {
|
||||
passwordResetTokenVersion: (
|
||||
passwordResetTokenVersion + 1
|
||||
).toString(),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return { email: decoded.email }
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -4,7 +4,33 @@ import { initTRPC } from '@trpc/server'
|
||||
// since it's not very descriptive.
|
||||
// For instance, the use of a t variable
|
||||
// is common in i18n libraries.
|
||||
const t = initTRPC.create()
|
||||
const t = initTRPC.create({
|
||||
errorFormatter: ({ error, shape }) => {
|
||||
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||
if (shape.data.stack) console.error(error)
|
||||
|
||||
return {
|
||||
message: 'Internal server error',
|
||||
code: shape.code,
|
||||
data: {
|
||||
code: shape.data.code,
|
||||
httpStatus: shape.data.httpStatus,
|
||||
path: shape.data.path,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: shape.message,
|
||||
code: shape.code,
|
||||
data: {
|
||||
code: shape.data.code,
|
||||
httpStatus: shape.data.httpStatus,
|
||||
path: shape.data.path,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Base router and procedure helpers
|
||||
export const router = t.router
|
||||
|
||||
Reference in New Issue
Block a user