likes
comments
collection
share

「13」next-shopping:地址模块+一些中间件

作者站长头像
站长
· 阅读数 3

地址模块

首先我们更新一下utils/alert.js,新增地址编辑的处理:

import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content'

const MySwal = withReactContent(Swal)

const alert = (icon, msg) =>
  MySwal.fire({
    position: 'center',
    icon,
    title: msg,
    showConfirmButton: false,
    timer: 2000,
  })

export const confirmAlert = ({ title, text, icon, confirmButtonText }) =>
  Swal.fire({
    title,
    text,
    icon,
    showCancelButton: false,
    confirmButtonColor: '#3085d6',
    confirmButtonText,
  })

export const editInfo = (type, title, patchData, token, isError, error) => {
  Swal.fire({
    title,
    input: 'text',
    inputAttributes: {
      autocapitalize: 'off',
    },
    confirmButtonText: '确认',
    showLoaderOnConfirm: true,
    preConfirm: data => {
      if (type === 'mobile') {
        if (data.length < 11 || data.length >= 12) {
          return Swal.showValidationMessage('请完整输入您的手机号码')
        }
        const mobile = Number(data)
        patchData({
          url: '/api/user',
          body: { mobile },
          token,
        })
      }

      if (type === 'name') {
        const name = data
        if (name.length < 3) {
          return Swal.showValidationMessage('姓氏不能少于三个字')
        }
        patchData({
          url: '/api/user',
          body: { name },
          token,
        })
      }

      if (type === 'address') {
        const address = data
        patchData({
          url: '/api/user',
          body: { address },
          token,
        })
      }
      if (isError) Swal.showValidationMessage(error?.data?.err)
    },
  })
}

export default alert

更新app/api/user/route.js,在成功更新用户信息过后增加返回地址字段

import { NextResponse } from 'next/server'

import db from '@/lib/db'
import User from '@/models/User'
import auth from '@/middleware/auth'
import sendError from '@/utils/sendError'

const uploadInfo = auth(async req => {
  try {
    const { id: userId } = JSON.parse(req.headers.get('userInfo'))
    const result = await req.json()

    await db.connect()
    await User.findByIdAndUpdate({ _id: userId }, { ...result })
    const newUser = await User.findOne({ _id: userId })
    await db.disconnect()

    return NextResponse.json(
      {
        msg: '已成功更新用户信息',
        user: {
          avatar: newUser.avatar,
          name: newUser.name,
          mobile: newUser.mobile,
          email: newUser.email,
          role: newUser.role,
          root: newUser.root,
          address: newUser.address,
        },
      },
      {
        status: 201,
      }
    )
  } catch (error) {
    return sendError(500, error.message)
  }
})

const getUsers = auth(async req => {
  try {
    const role = req.headers.get('userRole')
    if (role !== 'admin') return sendError(400, '无权操作')

    await db.connect()
    const users = await User.find().select('-password')
    await db.disconnect()

    return NextResponse.json(
      {
        users,
      },
      {
        status: 200,
      }
    )
  } catch (error) {
    return sendError(500, error.message)
  }
})

export const PATCH = uploadInfo
export const GET = getUsers

然后修改app/(main)/profile/addresses/page.js,修改地址页面

'use client'

import Image from 'next/image'
import { useDispatch, useSelector } from 'react-redux'

import { updateUser } from '@/store/slices/authSlice'
import { usePatchDataMutation } from '@/store/slices/fetchApiSlice'
import { BackButton, Icons } from '@/components'
import { editInfo } from '@/utils/alert'
import { useEffect } from 'react'

export default function Addresses() {
  const dispatch = useDispatch()
  const { token, user } = useSelector(state => state.auth)

  const [patchData, { data, isSuccess, isError, error }] = usePatchDataMutation()

  useEffect(() => {
    if (isSuccess) {
      dispatch(updateUser(data.user))
    }
  }, [isSuccess])

  const editAddressHandler = () => {
    editInfo('address', '请完整输入您的地址', patchData, token, isError, error)
  }
  console.log('user', user)
  return (
    <div>
      <BackButton>地址</BackButton>
      <div>
        {user?.address ? (
          <div className="px-5 flex-1">
            <div className="flex justify-between py-4 border-b border-gray-200">
              <p>{user.address}</p>
              {user.address ? (
                <Icons.Edit className="icon cursor-pointer" onClick={editAddressHandler} />
              ) : (
                <Icons.Plus className="icon cursor-pointer" onClick={editAddressHandler} />
              )}
            </div>
          </div>
        ) : (
          <div className="py-20 flex flex-col items-center gap-y-4">
            <div className="relative h-52 w-52">
              <Image src="/images/address.svg" layout="fill" alt="address" />
            </div>
            <p>暂无地址</p>
            <button
              className="border-2 border-red-600 text-red-600 flex items-center gap-x-3 px-3 py-2 rounded-lg"
              onClick={editAddressHandler}
            >
              <Icons.Location className="icon text-red-600" />
              <span>地址输入</span>
            </button>
          </div>
        )}
      </div>
    </div>
  )
}

效果如下,无地址的情况:

「13」next-shopping:地址模块+一些中间件

我们输入地址,可以看到更新用户信息的时候返回了整个用户信息,然后会更新本地store.user,就可以达到页面变动效果。

「13」next-shopping:地址模块+一些中间件

新增api

主要是新增一个获取当前用户的api,新增app/api/auth/user/route.js

import { NextResponse } from 'next/server'

import db from '@/lib/db'
import User from '@/models/User'
import sendError from '@/utils/sendError'
import auth from '@/middleware/auth'

const getUserInfo = auth(async req => {
  try {
    const { id: userId } = JSON.parse(req.headers.get('userInfo'))

    await db.connect()
    const user = await User.findOne({ _id: userId }).select('-password')
    await db.disconnect()

    return NextResponse.json(
      {
        user: {
          name: user.name,
          email: user.email,
          mobile: user.mobile,
          avatar: user.avatar,
          address: user.address,
          role: user.role,
          root: user.root,
        },
      },
      { status: 200 }
    )
  } catch (error) {
    return sendError(500, error.message)
  }
})

export const GET = getUserInfo

helpers

本节新建如下代码:做后续使用

「13」next-shopping:地址模块+一些中间件

新建helpers/api/api-handler.js

import { NextRequest, NextResponse } from 'next/server'

import { errorHandler, jwtMiddleware, validateMiddleware, identityMiddleware } from '@/helpers/api'

export function apiHandler(handler, { identity, schema } = {}) {
  return async (req, ...args) => {
    try {
      const json = await req.json()
      req.json = () => json
    } catch {}

    try {
      await jwtMiddleware(req)
      await identityMiddleware(req, identity)
      await validateMiddleware(req, schema)

      const responseBody = await handler(req, ...args)
      return NextResponse.json(responseBody || {})
    } catch (err) {
      console.log('global error handler', err)
      return errorHandler(err)
    }
  }
}

新建helpers/api/error-handler.js

import { NextResponse } from 'next/server'
import { setJson } from './set-json'

export function errorHandler(error) {
  if (typeof error === 'string') {
    const is404 = error.toLowerCase().endsWith('not found')
    const status = is404 ? 404 : 400
    return NextResponse.json(setJson({ message: error, code: status }), { status })
  }

  if (error.name === 'JsonWebTokenError') {
    return NextResponse.json(
      setJson({
        message: 'Unauthorized',
        code: 401,
      }),
      { status: 401 }
    )
  }

  console.error(error)
  return NextResponse.json(
    setJson({
      message: error.message,
      code: '500',
    }),
    { status: 500 }
  )
}

新建helpers/api/identity-middleware.js

import User from '@/models/User'
import db from '@/lib/db'

export async function identityMiddleware(req, identity) {
  if (!identity || identity === 'user') return

  const userId = req.headers.get('userId')
  db.connect()
  const user = await User.findOne({ _id: userId })
  db.disconnect()

  if (identity === 'admin' && user.role !== 'admin') {
    throw new Error('Unauthorized')
  }

  if (identity === 'root' && !user.root) {
    throw new Error('Unauthorized')
  }

  req.headers.set('userRole', user.role)
  req.headers.set('userRoot', user.root)
}

新建helpers/api/jwt-middleware.js

import { auth } from '@/helpers'

export async function jwtMiddleware(req) {
  if (isPublicPath(req)) {
    return
  }

  const id = auth.verifyToken(req)
  req.headers.set('userId', id)
}

function isPublicPath(req) {
  const publicPaths = ['POST:/api/auth/login', 'POST:/api/auth/logout', 'POST:/api/auth/register']
  return publicPaths.includes(`${req.method}:${req.nextUrl.pathname}`)
}

新建helpers/api/set-json.js

export const setJson = ({ code, message, data }) => {
  return {
    code: code || 0,
    message: message || 'ok',
    data: data || null,
  }
}

新建helpers/api/validate-middleware.js

export async function validateMiddleware(req, schema) {
  if (!schema) return

  const body = await req.json()
  const { error, data } = schema.safeParse(body)
  if (error?.errors) {
    throw `Validation error: ${error?.errors?.map(x => x.message).join(', ')}`
  }

  // update req.json() to return sanitized req body
  req.json = () => data
}


helpers/api/index.js将他们导出

export * from './api-handler'
export * from './error-handler'
export * from './identity-middleware'
export * from './jwt-middleware'
export * from './set-json'
export * from './validate-middleware'

这上面api下的主要用于接口的中间件

新建helpers/repo/user-repo.js

import bcrypt from 'bcrypt'

import db from '@/lib/db'
import User from '@/models/User'
import { createAccessToken } from '@/utils/generateToken'

const getAll = async () => {
  await db.connect()
  const users = await User.find().select('-password')
  await db.disconnect()
  return users
}

const update = async (id, params) => {
  await db.connect()
  const user = await User.findById(id)

  if (!user) throw '用户不存在'

  Object.assign(user, params)

  await user.save()
  await db.disconnect()
  return user
}

const create = async params => {
  const { name, email, password } = params
  await db.connect()
  if (await User.findOne({ email })) {
    throw 'email "' + email + '" 账户已存在'
  }
  const hashPassword = await bcrypt.hash(password, 12)
  const newUser = new User({ name, email, password: hashPassword })
  await newUser.save()
  await db.disconnect()

  return newUser
}

const authenticate = async ({ email, password } = {}) => {
  await db.connect()
  const user = await User.findOne({ email })
  if (!user) {
    throw '找不到此电子邮件的应用程序'
  }
  const isMatch = await bcrypt.compare(password, user.password)
  if (!isMatch) {
    throw '电子邮件地址或密码不正确'
  }
  const token = createAccessToken({ id: user._id })
  await db.disconnect()
  return {
    user: {
      name: user.name,
      email: user.email,
      role: user.role,
      root: user.root,
    },
    token,
  }
}

const updateRole = async (id, role) => {
  await db.connect()
  const user = await User.findById(id)
  if (!user) throw '用户不存在'
  await User.findByIdAndUpdate({ _id: id }, { role })
  await db.disconnect()
}

const _delete = async id => {
  await db.connect()
  const user = await User.findById(id)
  if (!user) throw '用户不存在'
  await User.findByIdAndDelete(id)
  await db.disconnect()
}

const resetPassword = async (id, password) => {
  const hashPassword = await bcrypt.hash(password, 12)
  await db.connect()
  const user = await User.findById(id)
  if (!user) throw '用户不存在'
  await User.findByIdAndUpdate({ _id: id }, { password: hashPassword })
  await db.disconnect()
}

const getById = async id => {
  try {
    await db.connect()
    const user = await User.findById(id)
    await db.disconnect()
    return user
  } catch {
    throw 'User Not Found'
  }
}

export const userRepo = {
  create,
  authenticate,
  getAll,
  getById,
  update,
  delete: _delete,
  updateRole,
  resetPassword,
}

并导出helpers/repo/index.js

export * from './user-repo'

主要是编写之后的数据库操作

新建helpers/auth.js

import jwt from 'jsonwebtoken'

export const auth = {
  verifyToken,
  createAccessToken,
}

function verifyToken(req) {
  const token = req.headers.get('authorization')
  const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
  const id = decoded.id
  return id
}

function createAccessToken(payload) {
  return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
    expiresIn: '1d',
  })
}

新建helpers/db.js

import mongoose from 'mongoose'

const connection = {}

async function connect() {
  if (connection.isConnected) {
    console.log('Using existing connection.')
    return
  }
  if (mongoose.connections.length > 0) {
    connection.isConnected = mongoose.connections[0].readyState
    if (connection.isConnected === 1) {
      console.log('Use previous connection')

      return
    }
    await mongoose.disconnect()
  }

  try {
    const db = await mongoose.connect(process.env.MONGODB_URL)
    console.log('New connection')
    connection.isConnected = db.connections[0].readyState
  } catch (error) {
    console.log(error)
    process.exit(1)
  }
}

async function disconnect() {
  if (connection.isConnected) {
    if (process.env.NODE_ENV === 'production') {
      await mongoose.disconnect()
      connection.isConnected = false
    } else {
      console.log('not disconnected')
    }
  }
}

const db = { connect, disconnect }
export default db

新建helpers/index.js

export * from './auth'
export * from './db'
export * from './repo'

目前我们把authdbrepo操作都收敛到helpers里面了,其他的比如lib后续可以删除了

代码地址:github.com/liyunfu1998…