Skip to content

w3labkr/nextjs14-authjs5-dashboard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nextjs14-authjs5-dashboard

This is a dashboard starter template for the NextJS 14 app router based on Auth.js v5.

Screenshots

screenshot

Table of Contents

Denpendencies

  • Next.js 14
  • Auth.js v5 + Prisma Adapter
  • Tailwindcss
  • Shadcn
  • Prisma
  • Zustand
  • React Query
  • Nodemailer
  • Browserslist
  • bcrypt.js
  • Jose (JSONWebToken)
  • Day.js
  • qs
  • cookies-next
  • React Icons

Folder and file Structure

The folder and file structure is based on nextjs app router next.js project structure.

.
├── actions/                    # Server Actions
├── app/                        # App Router
│   └── api/
│       ├── auth/               # Authentication
│       └── v1/                 # Public APIs
├── components/                 # React components
├── config/                     # Configuration for site
├── context/                    # Context
├── docs/                       # Documents
├── hooks/                      # Hooks
├── lib/                        # Utility functions
├── prisma/                     # Prisma Schema Location and Configuration
├── public/                     # Static assets to be served
│   └── [locales]/              # Internationalization
├── queries/                    # API
├── schemas/                    # Schema validations
├── screenshots/                # Screenshots
├── store/                      # State
├── types/                      # Type definitions
└── package.json

Getting Started

Clone the repository to the current directory.

git clone https://github.com/w3labkr/nextjs14-authjs5-dashboard.git .

Install all modules listed as dependencies.

npm install

Copy of the .env.example if the .env doesn't exist.

cp .env.example .env

Create an SQL migration file and execute it.

npx prisma migrate dev --name init

Start the development server.

npm run dev

Documents

CSRF

All Server Actions can be invoked by plain <form>, which could open them up to CSRF attacks. Behind the scenes, Server Actions are always implemented using POST and only this HTTP method is allowed to invoke them. This alone prevents most CSRF vulnerabilities in modern browsers, particularly due to Same-Site cookies being the default.

As an additional protection Server Actions in Next.js 14 also compares the Origin header to the Host header (or X-Forwarded-Host). If they don't match, the Action will be rejected. In other words, Server Actions can only be invoked on the same host as the page that hosts it. Very old unsupported and outdated browsers that don't support the Origin header could be at risk.

Server Actions doesn't use CSRF tokens, therefore HTML sanitization is crucial.

When Custom Route Handlers (route.tsx) are used instead, extra auditing can be necessary since CSRF protection has to be done manually there. The traditional rules apply there.

How to Think About Security in Next.js

Examples

ApiResponse

import { ApiResponse } from '@/lib/http'

export async function POST(req) {
  return ApiResponse.json(null)
  // { status: 'success', success: true, message: 'OK', data: null }

  return ApiResponse.json({})
  // { status: 'success', success: true, message: 'OK', data: {} }

  return ApiResponse.json({ message: 'hi' })
  // { status: 'success', success: true, message: 'hi', data: {} }

  return ApiResponse.json({ user: null })
  // { status: 'success', success: true, message: 'OK', data: { user: null } }

  return ApiResponse.json({ user: null, message: 'hi' })
  // { status: 'success', success: true, message: 'hi', data: { user: null } }

  return ApiResponse.json({ user: null }, { status: 400 })
  // { status: 'fail', success: false, message: 'Bad Request', data: { user: null } }

  return ApiResponse.json({ user: null, message: 'hi' }, { status: 400 })
  // { status: 'fail', success: false, message: 'hi', data: { user: null } }
}

http status codes and text

import { 
  STATUS_CODES,
  STATUS_TEXTS,
  STATUS_CODE_TO_TEXT,
  STATUS_TEXT_TO_CODE
} from '@/lib/http'

STATUS_CODES.OK // 200
STATUS_TEXTS.OK // "OK"
STATUS_CODE_TO_TEXT["200"] // "OK"
STATUS_TEXT_TO_CODE["OK"] // "200"

Password encryption and verification

import { generateHash, compareHash } from '@/lib/bcrypt'

const hashed = await generateHash('hash')

if (await compareHash('hash', hashed)) {
  // isMatch
}

Client-side CSRF protection

'use client'

import { useCsrfToken } from '@/hooks/use-csrf-token'

export function Component() {
  const csrfToken = useCsrfToken()

  async function onSubmit() {
    const res = await fetch('/api', { 
      headers: { 'X-CSRF-Token': csrfToken } 
    })
  }

  return <button onClick={onSubmit}>Submit</button>
}

Server-side CSRF protection

import { verifyCsrfToken } from '@/lib/crypto'

export async function POST(req) {
  if (!verifyCsrfToken(req)) {
    return new Response('Unauthorized', { status: 401 })
  }
}

Sending mail

import { transporter, sender } from '@/lib/nodemailer'

try {
  const info = await transporter.sendMail({
    from: `"${sender?.name}" <${sender?.email}>`,
    to: "receiver@sender.com",
    subject: "Message title",
    text: "Plaintext version of the message",
    html: "<p>HTML version of the message</p>",
  })
} catch (e) {
  console.log(e)
}

The time zone and localized format are set.

import dayjs from '@/lib/dayjs'

dayjs().toISOString()

LucideIcon

import { LucideIcon } from '@/components/lucide-icon'

<LucideIcon name="Heart"/>

Type Definition

JWT

import { 
  decodeJwt,
  verifyJwt,
  jwtSign,
  generateRecoveryToken,
  generateAccessToken,
  generateRefreshToken,
  generateTokenExpiresAt,
  isTokenExpired
} from '@/lib/jwt'

decodeJwt(jwt: string): PayloadType & JWTPayload
verifyJwt(jwt: string | Uint8Array, options?: JWTVerifyOptions): Promise<JSON | null>
jwtSign(sub: string, exp: number | string | Date = '1h', payload?: JWTPayload): Promise<string>
generateRecoveryToken(sub: string, payload?: JWTPayload): Promise<string>
generateAccessToken(sub: string): Promise<string>
generateRefreshToken(sub: string, jwt?: string | null): Promise<string>
generateTokenExpiresAt(expiresIn: number = 60 * 60): number
isTokenExpired(expiresAt: number, options?: { expiresIn?: number; expiresBefore?: number}): boolean

bcrypt

import {
  generateHash,
  compareHash
} from '@/lib/bcrypt'

generateHash(s: string): Promise<string>
compareHash(s: string, hash: string): Promise<boolean>

crypto

import {
  uuidv4,
  generateCsrfToken,
  verifyCsrfToken,
  verifyAjax,
  verifyCsrfAndAjax,
} from '@/lib/crypto'

uuidv4(): `${string}-${string}-${string}-${string}-${string}`
generateCsrfToken(size: number = 32): string
verifyCsrfToken(req: NextRequest): boolean
verifyAjax(req: NextRequest): boolean
verifyCsrfAndAjax(req: NextRequest): boolean

math

import {
  getRandom,
  getRandomArbitrary,
  getRandomInt,
  getRandomIntInclusive
} from '@/lib/math'

getRandom()
getRandomArbitrary(min: number, max: number)
getRandomInt(min: number, max: number)
getRandomIntInclusive(min: number, max: number)

utils

import {
  cn,
  wait,
  fetcher,
  absoluteUrl,
  relativeUrl
} from '@/lib/utils'

cn(...inputs: ClassValue[]): string
wait(milliseconds: number): Promise<void>
fetcher(input: RequestInfo | URL, init?: RequestInit): Promise<JSON>
absoluteUrl(url: string | URL, base?: string | URL): string
relativeUrl(url: string | URL, base?: string | URL): string

License

This software license under the MIT License.