Conversation

✨ I started a new app recently, and while I've used Laravel in the past for "heavy" apps, decided to go 100% with Next.js this time, as a challenge. As I'm building all the scaffolding from the app, I've borrowed some ideas from Laravel that JS devs might find interesting... πŸ‘‡
23
443
While I mostly use for the Queue these days (no other solution comes close, especially coupled with Vapor), you usually don't think about the work it does for you: πŸ“¨ Crafting & sending mail πŸ” Authentication & authorization With , you start from scratch. πŸ˜…
4
36
Replying to
This is the schema I want to use (using cuz it seems like the cleanest ORM for TypeScript, missing Eloquent tho πŸ˜…). You'll notice it doesn't include any session data or verification tokens (which you'd need for the magic login links). So, how did I make it work? 😁
A screenshot showing a Prisma schema for a User table, with "id", "name", "email", "createdAt", "updatedAt" columns.
3
25
First, let's cover the session. I'm using 's iron-session package, which lets you store a small (encrypted) object inside a cookie. πŸͺ I use this to store the ID of the user (and the current team as well in my case), and then fetch it from the database when I need them.
import prisma from '@/lib/prisma'

const handler = async (req, res) => {
	if (!req.session.userId) {
		return res.json({ authenticated: false })   
	}

	return res.json({
		authenticated: true,
		user: await prisma.user.findUnique({
			where: { id: req.session.userId }
        }),
	})
}

export default handler
2
32
Finally, what about the verification token? Here's where I'm taking a page from the Laravel playground, recreating their "signed links" feature. I'm encrypting an object (w/ Iron) with the user's email and an expiry date and using that as the token users get on their email! 🀩
import Iron from '@hapi/iron'
import { sendMagicLink } from '@/lib/mail'
const BASE_VERIFY_URL = "https://scripty.studio/api/auth/login"


const handler = async (req, res) => {
	if (!req.body.email) return res.status(400).end()

	const token = await Iron.seal({
		email: req.body.email,
		expires: now().add(1, 'hours').toDate()
    }, process.env.APP_KEY, Iron.defaults)

	await sendMagicLink(
		req.body.email,
		`${BASE_VERIFY_URL}?email=${req.body.email}&token=${token}`,   
	})

	res.status(200).json({ message: 'Email sent!' })
}
import Iron from '@hapi/iron'

const handler = async (req, res) => {
	const { email, token } = req.query
	if (!email || !token) return res.status(400).end()

	const loginData = await Iron.unseal(
		token as string, process.env.APP_KEY, Iron.defaults
    )

	if (email != loginData.email || loginData.expires < now().toDate()) {   
        // expired link (or someone is trying to swap the email lmeow)
		return res.status(400).end()
	}

	const user = await prisma.user.findUnique({ where: { email } })
	if (!user) return res.status(400).end()

	req.session.userId = user.id
	await req.session.save()

	res.redirect('/dashboard')
}
1
28
Note this has the downside that links will work for as long as they are active, so I'd recommend to set it to a short period (I'm using 1h). The upside is a much much cleaner database, and clean code I really feel like I understand (which is huge!) 😁
1
19
As a bonus note, let's talk about how I'm crafting and sending emails! πŸ“¬ I write my emails in JSX using mjml-react (which comes with a pretty nifty VSCode extension w/ live-previews). When it's time to send, you can render to html from node, and send it using nodemailer. πŸš€
Image
Image
3
35
And that's it! I'll keep tweeting lil DX hacks for making Next more manageable for big apps (at least from the perspective of someone used to MVC & lots of magic), so follow if you're into that i guess. Also, check out the app I'm building! I think it's pretty dope 😁 ✌️
Quote Tweet
Been working on a lil script-writing app πŸ‘€ it feels really crazy how fast I can go from "i wish this existed" to a prototype, coding really is a magical skill ✨ scripty.vercel.app
Show this thread
Image
Image
Image
4
30