Documentation

Authentication

Learn how authentication works in Dirstarter, including BetterAuth setup, providers, user management, and security

Dirstarter uses BetterAuth for authentication, providing a flexible and secure system with multiple authentication methods and role-based access control.

Core Features

  • Magic Link authentication with Resend integration
  • Social login providers (Google, GitHub)
  • Role-based access control (admin/user)
  • Session management
  • User management interface

Setup

The following environment variables are required:

.env
BETTER_AUTH_SECRET=your_secret_key // Random string
BETTER_AUTH_URL=http://localhost:3000

You can generate a random string for the secret key using openssl rand -base64 32.

For every OAuth provider you want to use, you need to set the clientId and clientSecret provider variables.

.env
# Google OAuth
AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secret

Follow the BetterAuth documentation for more information where to find the clientId and clientSecret.

Basic Configuration

Authentication is configured in lib/auth.ts:

lib/auth.ts
export const auth = betterAuth({
  database: prismaAdapter(db, {
    provider: "postgresql",
  }),
 
  socialProviders: {
    google: {
      clientId: env.AUTH_GOOGLE_ID,
      clientSecret: env.AUTH_GOOGLE_SECRET,
    },
  },
 
  account: {
    accountLinking: {
      enabled: true,
    },
  },
 
  plugins: [
    magicLink({
      sendMagicLink: async ({ email, url }) => {
        // Email sending logic
      },
    }),
 
    admin(),
 
    // Add other plugins here
  ],
})

For a full list of available configuration options, plugins and providers see the BetterAuth documentation.

Authentication Methods

By default, Dirstarter includes 2 authentication methods: Magic Link and Google Login.

Adding Authentication Methods

BetterAuth supports a wide range of authentication methods, including more social providers, email/password, passkeys and more.

Magic Link authentication allows users to sign in without a password. When a user enters their email, they receive a secure link that automatically logs them in.

components/web/auth/login-form.tsx
const handleSignIn = ({ email }: z.infer<typeof schema>) => {
  signIn.magicLink({
    email,
    callbackURL,
    fetchOptions: {
      onSuccess: () => {
        router.push(`/auth/verify?email=${email}`)
      },
      onError: ({ error }) => {
        toast.error(error.message)
      },
    },
  })
}

Social Login

Social login is supported through OAuth providers. Currently configured for Google:

components/web/auth/login-button.tsx
const handleSignIn = () => {
  signIn.social({
    provider,
    callbackURL,
    fetchOptions: {
      onRequest: () => {
        setIsPending(true)
      },
      onError: ({ error }) => {
        toast.error(error.message)
      },
    },
  })
}

User Roles

Dirstarter implements two user roles:

  • admin: Full access to all features and user management
  • user: Standard user access

User Management

Admins have access to various user management actions:

  • Edit user details
  • Change user roles
  • Ban/unban users
  • Revoke user sessions
  • Delete users

User Management

Protecting Routes and Actions

Route Protection

Use middleware to protect routes:

middleware.ts
export const config = {
  matcher: ["/admin/:path*", "/auth/:path*"],
}

Middleware auth check should not be the only protection for your routes. You should also protect routes in the route handler.

Protecting a page is done by checking the session in the page.

app/admin/page.tsx
import { auth } from "~/lib/auth"
import { redirect } from "next/navigation"
import { headers } from "next/headers"
 
export default async function AdminPage({ children }: { children: React.ReactNode }) {
  const session = await auth.api.getSession({ headers: await headers() })
 
  if (session?.user.role !== "admin") {
    redirect("/auth/login")
  }
 
  return <div>{children}</div>
}

Action Protection

There are 2 predefined server actions procedures that can be used to protect actions:

  • userProcedure: Protects actions that require a user to be authenticated
  • adminProcedure: Protects actions that require a user to be an admin
lib/safe-actions.ts
export const userProcedure = createProcedure().handler(async () => {
  const session = await auth.api.getSession({ headers: await headers() })
 
  if (!session?.user) {
    throw new Error("User not authenticated")
  }
 
  return { user: session.user }
})
 
export const adminProcedure = createProcedure(userProcedure).handler(async ({ ctx }) => {
  if (ctx.user.role !== "admin") {
    throw new Error("User not authorized")
  }
 
  return { user: ctx.user }
})

You can also create your own procedures and protect them with the same logic. For more information on how to create and use procedures, see the Zod Safe Actions documentation.

To use the procedures in your server actions, create them with the createServerAction function from ZSA:

actions/create-tool.ts
export const createTool = userProcedure
  .createServerAction()
  .input(z.object({
    name: z.string(),
    description: z.string(),
  }))
  .handler(async ({ input, ctx: { user } }) => {
    // Create a new tool
    const tool = await db.tool.create({
      data: {
        name: input.name,
        description: input.description,
        ownerId: user.id, // You can be certain that the user is authenticated
      },
    })
 
    return tool
  })

Client-Side Usage

Session Management

components/auth-button.tsx
'use client'
 
import { useSession, signIn, signOut } from '~/lib/auth-client'
 
export function AuthButton() {
  const { data: session, isPending } = useSession()
  
  if (isPending) {
    return <div>Loading...</div>
  }
  
  if (session) {
    return (
      <button onClick={() => signOut()}>
        Sign out
      </button>
    )
  }
  
  return (
    <button onClick={() => signIn.social({ provider: 'google' })}>
      Sign in with Google
    </button>
  )
}

Admin Checks

const { data: session } = useSession()
 
if (session?.user.role === 'admin') {
  // Show admin UI
}

Email Templates

Magic link emails use React Email for beautiful, responsive templates:

emails/login-link.tsx
const EmailLoginLink = ({ url, ...props }: EmailProps) => {
  return (
    <EmailWrapper {...props}>
      <Text>Welcome to {config.site.name}!</Text>
      <Text>Please click the magic link below to sign in to your account.</Text>
      <EmailButton href={url}>Sign in to {config.site.name}</EmailButton>
      <Text>or copy and paste this URL into your browser:</Text>
      <Text className="max-w-sm flex-wrap break-words font-medium leading-snug">{url}</Text>
    </EmailWrapper>
  )
}

Always implement proper authorization checks on both the client and server to secure your application.

Edit on GitHub

Last updated on

On this page