Create a Set Password Screen for a Social Media Site with Next.js | Full-Stack P

Started by wylmafuchsia, Oct 18, 2024, 08:15 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.


SEO

Thought for 24s
Next.js Set Password Screen (app Router, Tailwind + Shadcn/ui)
Set Password Feature for a Social Media App (Next.js 14 App Router)

This is a production‑ready Set/New Password flow built for the App Router. It includes:

A polished UI (Tailwind + shadcn/ui) with password strength meter and show/hide toggle

Zod + React Hook Form client-side validation

API route with token verification, bcrypt hashing, and Prisma example

Helpful UX details (disable on submit, toasts, error summaries)

Assumption: The user arrives at /set-password?token=... from an email link. The token was previously generated server-side and stored against the user (e.g., PasswordResetToken).

1) UI Page β€” app/(auth)/set-password/page.tsx
'use client'
    }
  }


  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-slate-50 to-slate-100 p-4">
      <Card className="w-full max-w-md shadow-xl border-slate-200">
        <CardContent className="p-6">
          <div className="flex items-center gap-2 mb-6">
            <div className="p-2 rounded-2xl bg-slate-100">
              <Shield className="w-5 h-5" />
            </div>
            <div>
              <h1 className="text-xl font-semibold">Set your new password</h1>
              <p className="text-sm text-slate-500">Keep your account safe and secure.</p>
            </div>
          </div>


          <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
            <div className="space-y-2">
              <Label htmlFor="password">New password</Label>
              <div className="relative">
                <Input
                  id="password"
                  type={show ? 'text' : 'password'}
                  placeholder="β€’β€’β€’β€’β€’β€’β€’β€’"
                  autoComplete="new-password"
                  {...register('password')}
                />
                <button
                  type="button"
                  onClick={() => setShow((s) => !s)}
                  className="absolute inset-y-0 right-2 grid place-items-center px-1 text-slate-500"
                  aria-label={show ? 'Hide password' : 'Show password'}
                >
                  {show ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
                </button>
              </div>
              {errors.password?.message && (
                <p className="text-xs text-red-600 mt-1">{errors.password.message}</p>
              )}
            </div>


            <PasswordStrengthBar password={password} />


            <div className="space-y-2">
              <Label htmlFor="confirm">Confirm password</Label>
              <Input id="confirm" type="password" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" {...register('confirm')} />
              {errors.confirm?.message && (
                <p className="text-xs text-red-600 mt-1">{errors.confirm.message}</p>
              )}
            </div>


            <Button type="submit" className="w-full" disabled={isSubmitting}>
              <Lock className="w-4 h-4 mr-2" />
              {isSubmitting ? 'Setting password...' : 'Set password'}
            </Button>


            {errors.root?.message && (
              <p className="text-xs text-red-600 mt-2">{errors.root.message}</p>
            )}
          </form>


          <p className="text-xs text-slate-500 mt-6">
            By continuing you agree to our Terms and acknowledge our Privacy Policy.
          </p>
        </CardContent>
      </Card>
    </div>
  )
}
2) Password Strength Bar β€” components/password-strength-bar.tsx
'use client'


import { useMemo } from 'react'
import { passwordScore } from '@/lib/password'


export function PasswordStrengthBar({ password }: { password: string }) {
  const score = useMemo(() => passwordScore(password), [password]) // 0..4
  const labels = ['Very weak', 'Weak', 'Okay', 'Strong', 'Excellent']
  const pct = ((score + 1) / 5) * 100


  return (
    <div className="space-y-1" aria-live="polite">
      <div className="h-2 w-full bg-slate-200/80 rounded-full overflow-hidden">
        <div
          className="h-full rounded-full transition-all"
          style={{ width: `${pct}%` }}
          role="progressbar"
          aria-valuemin={0}
          aria-valuemax={4}
          aria-valuenow={score}
          aria-label="Password strength"
        />
      </div>
      <p className="text-[11px] text-slate-500">{labels[Math.max(0, score)]}</p>
    </div>
  )
}
3) Password Utils β€” lib/password.ts
// Simple heuristic. For stronger analysis, consider zxcvbn-ts.
export function passwordScore(pw: string): 0 | 1 | 2 | 3 | 4 {
  let score = 0
  if (pw.length >= 8) score++
  if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++
  if (/[0-9]/.test(pw)) score++
  if (/[^A-Za-z0-9]/.test(pw)) score++
  if (pw.length >= 12) score++
  return Math.min(score, 4) as 0 | 1 | 2 | 3 | 4
}
4) API Route β€” app/api/auth/set-password/route.ts
import { NextResponse } from 'next/server'
import { z } from 'zod'
import bcrypt from 'bcryptjs'
import { prisma } from '@/lib/prisma' // export a singleton PrismaClient


const bodySchema = z.object({
  token: z.string().min(8),
  password: z.string().min(8).max(72)
})


export async function POST(req: Request) {
  try {
    const json = await req.json()
    const { token, password } = bodySchema.parse(json)


    // 1) Find reset token
    const reset = await prisma.passwordResetToken.findUnique({
      where: { token },
      include: { user: true }
    })


    if (!reset || reset.expiresAt < new Date() || reset.usedAt) {
      return NextResponse.json({ message: 'Token invalid or expired' }, { status: 400 })
    }


    // 2) Hash password
    const hash = await bcrypt.hash(password, 12)


    await prisma.$transaction([
      prisma.user.update({ where: { id: reset.userId }, data: { passwordHash: hash } }),
      prisma.passwordResetToken.update({ where: { id: reset.id }, data: { usedAt: new Date() } })
    ])


    // (Optional) Invalidate existing sessions here
    // e.g., delete from Session where userId = ...


    return NextResponse.json({ ok: true })
  } catch (err: any) {
    if (err?.name === 'ZodError') {
      return NextResponse.json({ message: 'Invalid request' }, { status: 400 })
    }
    console.error(err)
    return NextResponse.json({ message: 'Server error' }, { status: 500 })
  }
}
5) Prisma Models β€” prisma/schema.prisma
model User {
  id            String   @id @default(cuid())
  email         String   @unique
  passwordHash  String?
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
  // ...other profile fields
  ResetTokens   PasswordResetToken[]
}


model PasswordResetToken {
  id        String   @id @default(cuid())
  token     String   @unique
  userId    String
  user      User     @relation(fields: [userId], references: [id])
  createdAt DateTime @default(now())
  expiresAt DateTime
  usedAt    DateTime?
}
6) Token Generation (example) β€” app/api/auth/request-reset/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'
import { prisma } from '@/lib/prisma'


export async function POST(req: Request) {
  const { email } = await req.json()
  const user = await prisma.user.findUnique({ where: { email } })
  if (!user) return NextResponse.json({ ok: true }) // do not reveal existence


  const token = crypto.randomBytes(32).toString('hex')
  const expiresAt = new Date(Date.now() + 1000 * 60 * 60) // 1 hour


  await prisma.passwordResetToken.create({ data: { token, userId: user.id, expiresAt } })


  // Send email with link: `${process.env.NEXT_PUBLIC_APP_URL}/set-password?token=${token}`
  return NextResponse.json({ ok: true })
}
7) Minimal shadcn/ui setup

Install once:

npm i @radix-ui/react-icons lucide-react
# shadcn setup (if not already)
npx shadcn@latest init
npx shadcn@latest add button card input label
npm i react-hook-form zod bcryptjs @hookform/resolvers

Ensure Tailwind is configured and the shadcn UI components exist at @/components/ui/*.

8) Security Notes

Token lifetime: keep short (e.g., 15–60 minutes) and single-use.

Password hashing: use bcrypt (cost 12+) or argon2id. Never store raw passwords.

Session invalidation: after password change, revoke existing sessions.

Rate limiting: throttle both reset requests and set-password submissions.

**Error messag

I've dropped a complete, production-ready Set Password flow into the canvas β€” UI page, strength meter, API route with Prisma + bcrypt, and setup notes. If you want this tailored to pages router, another DB (Mongo/Mongoose, Supabase, Neon), or to plug into NextAuth, say the word and I'll adapt it.

Didn't find what you were looking for? Search Below