Admin客户端

Authentication System Guide

User Authentication and Session Management

Better Auth · Frontend Authentication · Backend Authentication


🎯 Overview

NextJS Base uses Better Auth as the authentication solution, supporting email/password login, OAuth login, and other authentication methods.

Tech Stack

ComponentDescription
Better AuthAuthentication framework
Prisma AdapterDatabase adapter
JWTToken management
CookiesSession storage

🔐 Better Auth

Configuration File

// lib/auth/auth.js
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from '@/lib/database/prisma'

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
  }),
  
  emailAndPassword: {
    enabled: true,
  },
  
  session: {
    expiresIn: 60 * 60 * 24 * 7,  // 7 days
    updateAge: 60 * 60 * 24,      // Update daily
  },
  
  // OAuth Configuration (Optional)
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    },
  },
})

Environment Variables

# Better Auth
BETTER_AUTH_SECRET="your-secret-key-at-least-32-characters"
BETTER_AUTH_URL="http://localhost:3000"

# OAuth (Optional)
GOOGLE_CLIENT_ID="xxx"
GOOGLE_CLIENT_SECRET="xxx"
GITHUB_CLIENT_ID="xxx"
GITHUB_CLIENT_SECRET="xxx"

🌐 Frontend Authentication

Client Configuration

// lib/auth/auth-client.js
import { createAuthClient } from 'better-auth/react'

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL,
})

export const {
  signIn,
  signUp,
  signOut,
  useSession,
} = authClient

Login Page

'use client'

import { useState } from 'react'
import { signIn } from '@/lib/auth/auth-client'
import { useRouter } from 'next/navigation'

export default function LoginPage() {
  const router = useRouter()
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault()
    setLoading(true)
    setError('')

    const formData = new FormData(e.target)
    const email = formData.get('email')
    const password = formData.get('password')

    try {
      const result = await signIn.email({
        email,
        password,
      })

      if (result.error) {
        setError(result.error.message)
      } else {
        router.push('/admin')
      }
    } catch (err) {
      setError('Login failed, please try again')
    } finally {
      setLoading(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Password" required />
      {error && <p className="error">{error}</p>}
      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  )
}

Register Page

'use client'

import { signUp } from '@/lib/auth/auth-client'

export default function RegisterPage() {
  const handleSubmit = async (e) => {
    e.preventDefault()
    const formData = new FormData(e.target)

    const result = await signUp.email({
      email: formData.get('email'),
      password: formData.get('password'),
      name: formData.get('name'),
    })

    if (result.error) {
      // Handle error
    } else {
      // Registration successful, redirect to login or auto login
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Username" required />
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Password" required />
      <button type="submit">Register</button>
    </form>
  )
}

Get Session

'use client'

import { useSession } from '@/lib/auth/auth-client'

export default function ProfilePage() {
  const { data: session, isPending } = useSession()

  if (isPending) {
    return <div>Loading...</div>
  }

  if (!session) {
    return <div>Please login first</div>
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>Email: {session.user.email}</p>
    </div>
  )
}

Sign Out

import { signOut } from '@/lib/auth/auth-client'

const handleLogout = async () => {
  await signOut()
  // Redirect to home or login page
}

🔒 Backend Authentication

Server-Side Get Session

// lib/auth/auth.js
import { auth as betterAuth } from './auth'
import { headers } from 'next/headers'

export const auth = async () => {
  const session = await betterAuth.api.getSession({
    headers: await headers(),
  })
  return session
}

Use in Server Action

'use server'

import { auth } from '@/lib/auth/auth'

export async function getUserProfileAction() {
  const session = await auth()
  
  if (!session?.user) {
    return { success: false, error: 'Please login first' }
  }
  
  // Get user information
  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
  })
  
  return { success: true, data: user }
}

Backend Access Control

// lib/auth/admin-auth.js
import { auth } from './auth'
import { prisma } from '@/lib/database/prisma'

export async function checkBackendAccess() {
  const session = await auth()
  
  if (!session?.user) {
    throw new Error('Please login first')
  }
  
  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
    select: { hasBackendAccess: true },
  })
  
  if (!user?.hasBackendAccess) {
    throw new Error('No backend access permission')
  }
  
  return session.user
}

Automatic Check in wrapAction

// wrapAction automatically handles authentication
export const sysGetUserListAction = wrapAction(
  'sysGetUserList',  // sys prefix automatically checks backend permission
  async (params, ctx) => {
    // ctx.userId - Current user ID
    // ctx.user - Current user information
    // ctx.isAdmin - Whether admin
    return await dao.getList(params)
  }
)

🛡️ Route Protection

Middleware Protection

// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
  const { pathname } = request.nextUrl
  
  // Backend route protection
  if (pathname.startsWith('/admin')) {
    const session = request.cookies.get('better-auth.session_token')
    
    if (!session) {
      return NextResponse.redirect(new URL('/auth/login', request.url))
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*'],
}

Page-Level Protection

// app/(admin)/admin/layout.js
import { auth } from '@/lib/auth/auth'
import { redirect } from 'next/navigation'

export default async function AdminLayout({ children }) {
  const session = await auth()
  
  if (!session?.user) {
    redirect('/auth/login')
  }
  
  // Check backend access permission
  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
  })
  
  if (!user?.hasBackendAccess) {
    redirect('/403')
  }
  
  return <>{children}</>
}

📊 User Model

Prisma Schema

model User {
  id                String    @id @default(cuid())
  email             String    @unique
  emailVerified     Boolean   @default(false)
  name              String?
  image             String?
  
  // Roles and permissions
  roles             String[]  @default([])
  hasBackendAccess  Boolean   @default(false)
  
  // Status
  banned            Boolean   @default(false)
  banReason         String?
  banExpires        DateTime?
  
  // Timestamps
  createdAt         DateTime  @default(now())
  updatedAt         DateTime  @updatedAt
  
  // Relations
  sessions          Session[]
  accounts          Account[]
}

model Session {
  id        String   @id @default(cuid())
  userId    String
  token     String   @unique
  expiresAt DateTime
  ipAddress String?
  userAgent String?
  
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  accountId         String
  providerId        String
  accessToken       String?
  refreshToken      String?
  accessTokenExpiresAt DateTime?
  refreshTokenExpiresAt DateTime?
  scope             String?
  
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  
  @@unique([providerId, accountId])
}