Recent Activity
Here's what @gregory has been up to this past year
-
Replied to Hi @gregory! By encryption tokens, are you referring to Signed...
Oh yes, I was actually mixing up
encryptionwithsignedUrlFor(the new method in v7). I was trying to understand when not to usesignedUrlFor, because it still feels a bit strange to rely on it without having any database tables behind it.I’m currently refactoring my code and removed the verification‑email and forgot/reset‑password tables in favor of
signedUrlFor. The only thing that feels odd is that you don’t really have a direct way to know whether the action was completed — for example, whether the password was reset, except by checking something like theupdated_atfield on the user, or maybe I should use the logger.
By the way, are you looking for someone to help you with coding (contribute to any of the projects you’re working on)? Just asking in case you ever need an extra pair of hands (Free of charge :)). -
Commented on post Forgot Password & Password Reset
I’ve been wondering about something for a while: Why use Adonis encryption tokens instead of node:crypto?
What bothers me is that encrypted tokens expire after a certain amount of time but can't be revoked. A user can request multiple tokens (until they hit the rate limit), and all of them remain valid until they expire. The upside is that the system is stateless, so you don’t need a database table.
With
node:cryptoand a dedicated table, you can revoke or invalidate tokens at any time.Maybe I’m overthinking it and should just use encrypted tokens because they’re simpler.
Another example is email changes: you usually send a token to the new email to confirm the change, and another to the old email so the user can revoke it. Without a table, I don’t see how this could work. If the user clicks the revoke link first, it won’t actually cancel the confirm token, you would have to wait until the confirm token is used or expires before the change is cancelled. With a table and hashed tokens, you can simply set a
canceled_attimestamp, which immediately invalidates the whole request.So maybe encrypted tokens should only be used for actions that aren’t cancellable, like email verification or password reset, and not for flows like email changes where revocation is required.
-
Replied to Hi @gregory! I've got good news, transactions cascade through...
Thanks! I ended up doing this before reading your post. I’m not sure which approach is better from an experienced developer’s point of view, but they both work.
I did read the documentation, but since I’m not a native English speaker, it didn’t occur to me that transactions cascade through relationships was the solution. Now it’s crystal clear. I feel a bit silly for missing it.
PS: Thank you for always replying to my questions.const result = await db.transaction(async (trx) => { const user = await User.query({ client: trx }).where('email', payload.email).first() const { token, hash } = generateTokenAndHash() if (!user) return null await RevokePasswordResetTokens.handle({ user }) await CreatePasswordResetToken.handle({ user, hash }) return { user, token } }) if (result) { PasswordResetRequested.dispatch(result.user, result.token) } return { status: 'sent' }Copied! -
Commented on post Saving All Or Nothing with Database Transactions
Hi @tomgobich
I’m having trouble handling transactions after my refactor. How would you approach this?type Params = { payload: Infer<typeof forgotPasswordValidator> } type SendPasswordResetResult = { status: 'sent' } | { status: 'rate_limited' } @inject() export default class SendPasswordResetEmail { constructor(protected rateLimiterService: RateLimiterService) {} async handle({ payload }: Params): Promise<SendPasswordResetResult> { const ipLimiter = this.rateLimiterService.forgotPasswordIpLimiter() const emailLimiter = this.rateLimiterService.forgotPasswordEmailLimiter() const ipKey = this.rateLimiterService.forgotPasswordIpKey() const emailKey = this.rateLimiterService.forgotPasswordEmailKey(payload.email) try { await ipLimiter.consume(ipKey) await emailLimiter.consume(emailKey) const user = await User.query().where('email', payload.email).first() const { token, hash } = generateTokenAndHash() if (!user) return { status: 'sent' } // Transaction is needed here await RevokePasswordResetTokens.handle({ user }) await CreatePasswordResetToken.handle({ user, hash }) PasswordResetRequested.dispatch(user, token) return { status: 'sent' } } catch (error) { if (error instanceof errors.E_TOO_MANY_REQUESTS) { return { status: 'rate_limited' } } throw error } } }Copied!// This class revokes all active password‑reset tokens for the user export default class RevokePasswordResetTokens { static async handle({ user }: Params) { await validPasswordResetTokens(user).update({ revokedAt: DateTime.now(), revokedBy: 'user', revokedReason: 'new_token_requested', }) } }Copied!// This query returns all valid password‑reset tokens for the user, export function validPasswordResetTokens(user: User) { return user .related('passwordResetTokens') .query() .whereNull('usedAt') .whereNull('revokedAt') .where('expiresAt', '>=', DateTime.now().toSQL()) }Copied!// This class creates a new password‑reset tokens record, // storing a hash that expires in 15 minutes. export default class CreatePasswordResetToken { static async handle({ user, hash }: Params) { await user.related('passwordResetTokens').create({ hash, expiresAt: DateTime.now().plus({ minutes: 15 }), }) } }Copied!I suppose I have to start it like this, but after that I’m stuck:
await db.transaction(async (trx) => { await RevokePasswordResetTokens.handle({ user, trx }) await CreatePasswordResetToken.handle({ user, hash, trx }) })Copied! -
Completed lesson How To Add Social Authentication with AdonisJS Ally & Google
-
Completed lesson Rebuilding Jagr.Co, Username Sign In & Post CRUD
-
Replied to Hey @gregory, great question! For most things, applying the ...
Thanks for your help, everything makes sense now. Hopefully it helps others with the same question.
-
Commented on post Clearing Login Attempt Rate Limits on Password Reset
Hey @tomgobich !
I’ve been watching your videos and checking out the Adocasts GitHub repo. I’m curious, when is it better to use a rate limiter inside an action or service, and when should it be applied at the route level?
For example, in Adocasts you use this code for the signup route and apply the rate limiter at the router levelexport const throttleSignUp = limiter.define('signUp', (ctx) => { const ip = GetIpAddress.run(ctx.request) return limiter .allowRequests(3) .every('1 hour') .blockFor('6 hours') .usingKey(`sign_up_ip_${ip}`) .limitExceeded((error) => error.setMessage("You've created too many accounts. Please wait before creating another.") ) })Copied!router.post('/signup', [StoreSessionSignUp]).as('auth.signup.store').use([middleware.guest(), middleware.turnstile(), throttleSignUp])Copied!Thank you in advance, and thanks again for always taking the time to reply to my questions.
-
Completed lesson Use Slots To Make A Button Component
-
Completed lesson Creating Our Base Button
-
Completed lesson Serializing Props as Element Attributes
-
Completed lesson A Look At Component Reactivity
-
Completed lesson Dismissible & Self Destructing Alerts
-
Completed lesson Inverse Alert Style & Cascading Slots
-
Completed lesson Adding Conditional Icon, Headline, and Message Content
-
Completed lesson Adding Alert Variants
-
Completed lesson Remembering A User's Authenticated Session
-
Completed lesson Logging In An Existing User
-
Completed lesson Logging Out An Authenticated User
-
Completed lesson Checking For and Populating an Authenticated User
-
Completed lesson Authenticating A Newly Registered User
-
Completed lesson The Flow of Middleware
-
Completed lesson How To Create A Custom VineJS Validation Rule
-
Completed lesson Creating A Login Form and Validator
-
Completed lesson Creating An EdgeJS Form Input Component
-
Completed lesson Reusing Old Form Values After A Validation Error
-
Completed lesson Displaying Validation Errors and Validating from our Request
-
Completed lesson Validating Form Data with VineJS
-
Completed lesson Accepting Form Data
-
Commented on post Forgot Password & Password Reset
Hello @tomgobich
Why not using.preload('user')instead ofconst user = await token?.related('user').query.first? Is usingpreloadin this case a bad practice?const token = await PasswordResetToken.query() .where('value', value) .preload('user') .first()Copied!You can then
`return { isValid: token?.valid, token, user: token?.user } -
Completed lesson Creating Our Base Alert
-
Completed lesson Dynamic Demo Routes for Components
-
Completed lesson View Structure and Component-Based Layouts
-
Completed lesson Getting Started, Installing AlpineJS and TailwindCSS
-
Completed lesson Exploring EdgeJS' Component System
-
Completed lesson Component Tags, State, and Props
-
Completed lesson Making A Reusable Movie Card Component
-
Completed lesson HTML Attribute and Class Utilities
-
Completed lesson EdgeJS Templating Basics
-
Completed lesson Easy Imports with NodeJS Subpath Imports
-
Completed lesson Quick Start Apps with Custom Starter Kits
-
Completed lesson Environment Variables and their Validation
-
Completed lesson Singleton Services and the Idea of Caching
-
Completed lesson Defining A Structure for our Movie using Models
-
Completed lesson Cleaning Up Routes with Controllers
-
Completed lesson Extracting Reusable Code with Services
-
Completed lesson Listing Movies from their Markdown Files
-
Completed lesson Reading and Supporting Markdown Content
-
Completed lesson Setting Up Tailwind CSS
-
Completed lesson Vite and Our Assets
-
Completed lesson Validating Route Parameters
-
Completed lesson Loading A Movie Using Route Parameters
-
Completed lesson Linking Between Routes
-
Completed lesson Rendering a View for a Route
-
Completed lesson Routes and How To Create Them
-
Completed lesson VS Code Extensions and Configuration
-
Completed lesson Project Structure
-
Completed lesson Creating A New AdonisJS 6 Project
-
Completed lesson What We'll Need Before We Begin
-
Completed lesson Introducing AdonisJS
-
Commented on post Updating Our URL & Populating Filter Field Values
Hello @tomgobich
Do you think it’s reasonable to build a booking platform for cooking classes using Adonis and HTMX? I’m a junior developer, and since I’m building it alone, creating a backend with Adonis and a React frontend feels like a huge amount of work. It seems like that stack would be overkill, especially since I don’t need my app to be on the Apple or Google Play store. What would be the reason to choose React over HTMX?Thanks in advance for your reply.
-
Completed lesson Updating Our URL & Populating Filter Field Values
-
Completed lesson Creating Our Filter Query with AdonisJS
-
Completed lesson Installing HTMX & Project Overview
-
Completed lesson Clearing Login Attempt Rate Limits on Password Reset
-
Completed lesson Deleting Items and Flushing our Redis Cache
-
Completed lesson Improved Caching with Redis
-
Completed lesson Restricting Login Attempts with Rate Limiting
-
Commented on post Default Layouts & Overwriting the Default Layout
I spent the afternoon making it work with react.
@inertiajs/reacthas a type calledResolvedComponent(alias) type ResolvedComponent = ComponentType<any> & { layout?: LayoutComponent | LayoutComponent[] | LayoutFunction; } import ResolvedComponentCopied!I imported
ResolvedComponentand used it to typeimport.meta.globinapp.tsxandssr.tsx. I then followed the code provided in the Inertia documentation to add a fallback to a default layout./// <reference path="../../adonisrc.ts" /> /// <reference path="../../config/inertia.ts" /> import '../css/app.css' import { hydrateRoot } from 'react-dom/client' import { createInertiaApp, type ResolvedComponent } from '@inertiajs/react' import { resolvePageComponent } from '@adonisjs/inertia/helpers' import { ReactNode } from 'react' import GuestLayout from '~/layouts/GuestLayout' const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS' createInertiaApp({ progress: { color: '#5468FF' }, title: (title) => `${title} - ${appName}`, resolve: async (name) => { const page = await resolvePageComponent( `../pages/${name}.tsx`, import.meta.glob<{ default: ResolvedComponent }>('../pages/**/*.tsx') ) page.default.layout = page.default.layout || ((page: ReactNode) => <GuestLayout children={page} />) return page }, setup({ el, App, props }) { hydrateRoot(el, <App {...props} />) }, })Copied!import ReactDOMServer from 'react-dom/server' import { createInertiaApp, type ResolvedComponent } from '@inertiajs/react' import GuestLayout from '~/layouts/GuestLayout' import { ReactNode } from 'react' export default function render(page: any) { return createInertiaApp({ page, render: ReactDOMServer.renderToString, resolve: (name) => { const pages = import.meta.glob<{ default: ResolvedComponent }>('../pages/**/*.tsx', { eager: true, }) const page = pages[`../pages/${name}.tsx`] page.default.layout = page.default.layout || ((page: ReactNode) => <GuestLayout children={page} />) return page }, setup: ({ App, props }) => <App {...props} />, }) }Copied!You can apply a layout of your choice to your pages like this:
import { ReactNode } from 'react' import { RegisterForm } from '~/components/register-form' import AuthLayout from '~/layouts/AuthLayout' export default function Register() { return ( <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10"> <div className="w-full max-w-sm"> <RegisterForm /> </div> </div> ) } Register.layout = (page: ReactNode) => <AuthLayout children={page} />Copied! -
Completed lesson Onboarding Newly Registered Users
-
Completed lesson Logging Out Users
-
Completed lesson Deferring A Prop Load Until it is Visible in InertiaJS 2
-
Completed lesson Defer Loading Props in InertiaJS 2
-
Completed lesson Prefetching Page to Boost Load Times in InertiaJS 2
-
Completed lesson What Code Can & Can't Be Shared Between AdonisJS & Inertia
-
Completed lesson Specifying Page Titles & Meta Tags
-
Completed lesson Default Layouts & Overwriting the Default Layout
-
Completed lesson Partial and Lazy Data Loading and Evaluation
-
Completed lesson Global Components and Hydration Mismatch in Action
-
Completed lesson The Link Component and Programmatic Linking
-
Completed lesson Linking Between Pages & Page State Flow
-
Completed lesson Sharing Data from AdonisJS to Vue via Inertia
-
Completed lesson The Flow of Pages and Page Props
-
Completed lesson Setting Up TailwindCSS, Shadcn-Vue, and Automatic Component Imports
-
Completed lesson Server-Side Rendering (SSR) vs Client-Side Rendering (CSR)
-
Completed lesson Creating Our AdonisJS App With InertiaJS
-
Completed lesson What We'll Be Building
-
Completed lesson What Is InertiaJS?
-
Completed lesson Adding the Remember Me Token
-
Completed lesson Logging In Users & Displaying Exceptions
-
Commented on post User Registration with InertiaJS
Hello Tom,
I would like to implement email verification, but I’m not sure what I should do in thestoremethod of theRegisterController.
I would like to show this message to the user after registration:Verify your email
You need to verify your email to activate your account..
A verification link has been sent to your email address. Please check your inbox and click the link to verify your email.
The link is valid for the next 15 minutes.
If you don’t see the email, check your spam or junk folder.
Should I use
return inertia.render('auth/email-verification')orreturn response.redirect().toRoute('email.verify.index')?export default class RegisterController { async show({ inertia }: HttpContext) { return inertia.render('auth/register') } async store({ request, response, inertia }: HttpContext) { const payload = await request.validateUsing(registerValidator) const role = await Role.getBySlug('user') const user = await User.create({ roleId: role.id, firstName: payload.firstName, lastName: payload.lastName, unconfirmedEmail: payload.email, password: payload.password, }) const signedUrl = router .builder() .prefixUrl(env.get('BASE_URL')) .params({ id: user.id, }) .qs({ email: encryption.encrypt(user.unconfirmedEmail, '15 minutes'), }) .makeSigned('email.verify', { expiresIn: '15 minutes', }) await mail.sendLater(new VerificationEmailNotification(user, signedUrl)) // return response.redirect().toRoute('email.verify.index') // or // return inertia.render('auth/email-verification') } }Copied!If I use
inertia.render('auth/email-verification'), it will display my React page but remain onapp_url/register.router .get('/email/verify/:id', [EmailVerificationController, 'verify']) .as('email.verify') .use(middleware.guest()) export default class EmailVerificationController { async verify({ request, response }: HttpContext) { if (!request.hasValidSignature()) { return response.badRequest('Invalid or expired URL') } const email = encryption.decrypt(request.qs()['email'] ?? '') const id = request.param('id') const user = await User.findOrFail(id) if (email !== user.unconfirmedEmail) { return response.badRequest('Invalid or expired URL') } user.status = UserStatus.ACTIVE user.email = user.unconfirmedEmail user.unconfirmedEmail = null user.verifiedAt = DateTime.now() await user.save() console.log(user) } }Copied!If I use
return response.redirect().toRoute('email.verify.index'), it will redirect toapp_url/email/verify/sent. This mean I will need to declare two routes instead of one.router .get('/email/verify/sent', [EmailVerificationController, 'index']) .as('email.verify.index') .use(middleware.guest()) router .get('/email/verify/:id', [EmailVerificationController, 'verify']) .as('email.verify') .use(middleware.guest()) export default class EmailVerificationController { async index({ inertia }: HttpContext) { return inertia.render('auth/email-verification') } async verify({ request, response }: HttpContext) { // logic here } }Copied! -
Completed lesson Forgot Password & Password Reset
-
Completed lesson Creating A Layout
-
Completed lesson Splitting Our Routes Between Auth & Web
-
Completed lesson User Registration with InertiaJS
-
Completed lesson Creating A Toast Message Manager
-
Completed lesson Completing Our AppLayout & Navigation Bar
-
Completed lesson Typing Lucid Models in Inertia with DTOs
-
Completed lesson Seeding Our Initial Database Data
-
Completed lesson Creating A Lucid Model Mixin for our Organization Relationship
-
Completed lesson Defining Our Lucid Models & Relationships
-
Completed lesson Defining Our Migrations & Foreign Keys
-
Completed lesson Understanding Our Database Schema
-
Completed lesson What Are Some of Inertia's Limitations
-
Completed lesson Cross-Site Request Forgery (CSRF) Protection in InertiaJS
-
Completed lesson Creating A FormInput Vue Component
-
Completed lesson Common useForm Methods & Options
-
Completed lesson The useForm Helper
-
Completed lesson Form Validation & Displaying Errors
-
Completed lesson Super Easy Infinite Scroll in InertiaJS 2 with Prop Merging
-
Completed lesson Polling for Changes in InertiaJS 2
-
Completed lesson Upgrading to Inertia 2
-
Completed lesson Inertia Form Basics
-
Account created Welcome to Adocasts, @gregory!