Lesson 07-Next.js Application Routing-Configuration and Authentication

Basic Configuration

Next.config.js Configuration

The next.config.js file is used to customize Next.js build configurations.

Example Configuration

// next.config.js
const path = require('path');

module.exports = {
  reactStrictMode: true,
  swcMinify: true,
  images: {
    domains: ['example.com'],
  },
  experimental: {
    appDir: true, // Enable the new App Router
    serverComponents: true,
    // Other experimental features...
  },
  // Define webpack aliases
  webpack(config) {
    config.resolve.alias = {
      ...config.resolve.alias,
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    };
    return config;
  },
  // Other configuration options...
};

Configuration Options

  • reactStrictMode: Enables React.StrictMode.
  • swcMinify: Uses SWC for code minification.
  • images: Configures image optimization options.
  • experimental: Enables experimental features like the new App Router and Server Components.

TypeScript Configuration

Next.js supports TypeScript, configurable via the tsconfig.json file.

Example Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

ESLint Configuration

ESLint is used to enforce code style and detect potential errors.

Example Configuration

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint",
    "plugin:prettier/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "rules": {
    "prefer-const": "warn",
    "@typescript-eslint/no-empty-function": "off",
    "@typescript-eslint/no-explicit-any": "off"
  }
}

Environment Variables Configuration

Next.js supports managing environment variables using .env files.

Example Configuration

// .env
NEXT_PUBLIC_API_URL=https://api.example.com

Using Environment Variables in Code

// app/page.js
console.log(process.env.NEXT_PUBLIC_API_URL);

Absolute Imports and Module Path Aliases

Absolute imports allow importing modules directly from the project root without relative paths. Module path aliases enable defining shorthand aliases for specific directories.

Configure TypeScript

Define path aliases in tsconfig.json.

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src", // Set source directory as base path
    "paths": {
      "@components/*": ["components/*"], // Define aliases
      "@utils/*": ["utils/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

Using Aliases

// app/page.js
import MyComponent from '@components/MyComponent'; // Using alias

Source Directory

By default, Next.js uses the pages directory as the source directory. You can change this via next.config.js.

Example Configuration

// next.config.js
module.exports = {
  pageExtensions: ['page.tsx', 'page.ts'], // Specify source file extensions
  // Other configuration options...
};

Draft Mode

Draft mode allows previewing unpublished changes, useful for content editing.

Enable Draft Mode

Enable draft mode in next.config.js.

// next.config.js
module.exports = {
  experimental: {
    unstable_useDraftMode: true, // Enable draft mode
  },
  // Other configuration options...
};

Using Draft Mode

Use the draftMode method in API routes.

// pages/api/draft.js
export default function handler(req, res) {
  if (req.method === 'POST') {
    res.unstable_setDraftMode({ enable: true });
    res.redirect('/');
  } else {
    res.status(405).end('Method Not Allowed');
  }
}

Content Security Policy (CSP)

Content Security Policy is a security feature that helps defend against cross-site scripting (XSS) attacks.

Configure CSP

Configure CSP in next.config.js.

// next.config.js
const withCsp = require('next-content-security-policy');

module.exports = withCsp({
  csp: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:"],
      connectSrc: ["'self'"],
      frameSrc: ["'none'"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      fontSrc: ["'self'"],
      childSrc: ["'self'"],
      formAction: ["'self'"],
      baseUri: ["'self'"],
      manifestSrc: ["'self'"],
      workerSrc: ["'self'", "blob:"],
      pluginTypes: ["application/pdf"],
    },
  },
  // Other configuration options...
});

Authentication

Technology Stack

  • Next.js 14: Core framework for building the application.
  • Node.js: Backend server.
  • Express: Building backend APIs.
  • jsonwebtoken: Generating and verifying JWTs.
  • passport: OAuth 2.0 authentication middleware.
  • passport-oauth2: OAuth 2.0 strategy.

Steps

  1. Set up project structure
  2. Create backend API
  3. Implement OAuth 2.0 authorization
  4. Generate and verify JWT
  5. Protect routes
  6. Project structure

Project Structure

Assume the following project structure:

my-app/
├── frontend/
│   ├── pages/
│   │   └── index.js
│   ├── public/
│   ├── src/
│   │   ├── api/
│   │   │   └── auth.js
│   │   └── utils/
│   │       └── jwt.js
│   ├── next.config.js
│   └── package.json
└── backend/
    ├── routes/
    │   └── auth.js
    ├── controllers/
    │   └── authController.js
    ├── passport/
    │   └── oauth2.js
    ├── middleware/
    │   └── authMiddleware.js
    ├── models/
    │   └── User.js
    ├── app.js
    └── package.json

Create Backend API

Install Dependencies

cd backend
npm install express passport passport-oauth2 jsonwebtoken bcryptjs dotenv

Create app.js

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const passport = require('passport');
const authRoutes = require('./routes/auth');

const app = express();

app.use(bodyParser.json());
app.use(cors());
app.use(passport.initialize());

app.use(authRoutes);

app.listen(3001, () => {
  console.log('Server is running on port 3001');
});

Implement OAuth 2.0 Authorization

Create passport/oauth2.js

const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2').Strategy;
const User = require('../models/User');

passport.use(new OAuth2Strategy(
  {
    authorizationURL: 'http://localhost:3001/auth/authorize',
    tokenURL: 'http://localhost:3001/auth/token',
    clientID: 'your-client-id',
    clientSecret: 'your-client-secret',
    callbackURL: 'http://localhost:3000/callback',
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      let user = await User.findOne({ oauthId: profile.id });
      if (!user) {
        user = await User.create({ oauthId: profile.id, username: profile.username });
      }
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

Generate and Verify JWT

Create middleware/authMiddleware.js

const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;

function generateToken(user) {
  return jwt.sign({ id: user.id }, secret, { expiresIn: '1h' });
}

function verifyToken(token) {
  return jwt.verify(token, secret);
}

module.exports = {
  generateToken,
  verifyToken,
};

Protect Routes

Create routes/auth.js

const express = require('express');
const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware');
const authController = require('../controllers/authController');

router.post('/login', authController.login);
router.post('/register', authController.register);
router.get('/protected', authMiddleware.protect, (req, res) => {
  res.send('Protected route');
});

module.exports = router;

Implement Controller Logic

Create controllers/authController.js

const bcrypt = require('bcryptjs');
const jwt = require('../middleware/authMiddleware');
const User = require('../models/User');

async function register(req, res) {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    const user = await User.create({
      username: req.body.username,
      password: hashedPassword,
    });
    const token = jwt.generateToken(user);
    res.json({ token });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
}

async function login(req, res) {
  try {
    const user = await User.findOne({ username: req.body.username });
    if (!user || !(await bcrypt.compare(req.body.password, user.password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    const token = jwt.generateToken(user);
    res.json({ token });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
}

module.exports = {
  register,
  login,
};

Frontend Implementation

Create src/api/auth.js

import axios from 'axios';

export async function login(data) {
  const response = await axios.post('/api/auth/login', data);
  return response.data;
}

export async function register(data) {
  const response = await axios.post('/api/auth/register', data);
  return response.data;
}

Create pages/index.js

import { useState } from 'react';
import { login, register } from '../src/api/auth';

export default function Home() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      const result = await login({ username, password });
      localStorage.setItem('token', result.token);
      console.log('Logged in successfully');
    } catch (error) {
      console.error(error);
    }
  };

  const handleRegister = async () => {
    try {
      const result = await register({ username, password });
      localStorage.setItem('token', result.token);
      console.log('Registered and logged in successfully');
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
      <button onClick={handleLogin}>Login</button>
      <button onClick={handleRegister}>Register</button>
    </div>
  );
}
Share your love