This guide provides a complete walkthrough for building a Next.js application featuring user authentication with NextAuth.js and integrating Sinch to handle inbound SMS messages, enabling basic two-way communication capabilities.
We'll cover everything from initial project setup and authentication implementation to receiving and processing Sinch webhooks, securing your application, and deploying it. By the end, you'll have a functional application where authenticated users can potentially receive SMS messages sent to a dedicated Sinch number, linked to their account via their phone number.
Project Overview and Goals
What We're Building:
A web application built with Next.js (App Router) that:
- Authenticates users using email/password credentials via NextAuth.js.
- Associates a phone number with each user profile (using E.164 format).
- Listens for inbound SMS messages sent to a configured Sinch phone number via webhooks.
- Verifies the authenticity of incoming Sinch webhooks using shared secrets.
- Identifies the user associated with the sender's phone number (assuming E.164 format).
- Stores the inbound message content in a database, linked to the user.
- (Foundation for Extension) Provides the basis to trigger replies or other actions based on incoming messages.
Problem Solved:
This guide addresses the need for applications to programmatically receive and process SMS messages from users, linking them to authenticated user accounts within a modern web framework like Next.js. It provides a secure and scalable foundation for features like SMS-based notifications, customer support interactions, or verification processes.
Technologies Used:
- Next.js (v14+): React framework for building server-rendered applications (using App Router).
- NextAuth.js (v5 Beta): Authentication library for Next.js, simplifying login, logout, and session management. (Disclaimer: This guide uses
next-auth@beta
(v5). While powerful, beta versions may contain bugs or breaking changes. For maximum stability in production, consider using the latest stable v4 release or carefully test v5 before deploying.) - Sinch: Communications Platform as a Service (CPaaS) provider for SMS functionality (specifically receiving inbound messages via webhooks).
- PostgreSQL: Relational database for storing user and message data.
- Prisma: Next-generation ORM for database access and migrations.
- TypeScript: For static typing and improved code quality.
- bcryptjs: For securely hashing user passwords.
- Zod: For data validation (credentials, potentially webhook payloads).
- Tailwind CSS: For styling (optional, included with
create-next-app
).
System Architecture:
graph TD
A[User Browser] -- HTTPS --> B(Next.js App / Vercel);
B -- Login Request --> C{NextAuth.js Middleware};
C -- Verify Credentials --> D[Database (PostgreSQL)];
D -- User Record --> C;
C -- Session Cookie --> A;
E[User's Phone] -- Sends SMS --> F(Sinch Platform);
F -- Inbound SMS Webhook (POST) --> G[Next.js API Route (/api/webhooks/sinch)];
G -- Verify Signature --> F;
G -- Lookup User by Phone (E.164) --> D;
G -- Store Message --> D;
D -- User/Message Data --> G;
G -- 200 OK --> F;
style G fill:#f9f,stroke:#333,stroke-width:2px
style F fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#ff9,stroke:#333,stroke-width:2px
Prerequisites:
- Node.js (v18 or later recommended) and npm/pnpm/yarn.
- Access to a PostgreSQL database instance.
- A Sinch account with API credentials and a provisioned SMS-enabled phone number.
- Basic understanding of React, TypeScript, and REST APIs.
openssl
command line tool for generating secrets. (Alternatives for Windows include Git Bash, Windows Subsystem for Linux (WSL), or online generation tools - use trusted tools for security-sensitive secrets).- A tool for testing webhooks locally (e.g., ngrok, Vercel CLI). Production alternatives include public IP addresses or other tunneling services.
Final Outcome:
A deployed Next.js application where users can register/login. Incoming SMS messages sent to the configured Sinch number will be securely received, verified, associated with the correct user based on their registered phone number (assuming consistent E.164 format), and stored in the database.
1. Setting up the Project
Let's initialize our Next.js project and install the necessary dependencies.
1.1 Initialize Next.js Project
Open your terminal and run:
npx create-next-app@latest sinch-inbound-app --typescript --eslint --tailwind --src-dir --app --use-npm # or pnpm/yarn
cd sinch-inbound-app
Follow the prompts, ensuring you enable TypeScript, ESLint, Tailwind CSS, the src/
directory, and the App Router.
1.2 Install Dependencies
npm install next-auth@beta @next-auth/prisma-adapter prisma @prisma/client pg bcryptjs @types/bcryptjs zod @sinch/sdk-core
npm install --save-dev prisma
next-auth@beta
: Authentication library (See disclaimer above regarding beta status).@next-auth/prisma-adapter
: Prisma adapter for NextAuth.prisma
,@prisma/client
: Prisma ORM and client.pg
: PostgreSQL driver (required by Prisma).bcryptjs
,@types/bcryptjs
: Password hashing library and its types.zod
: Schema validation library.@sinch/sdk-core
: Optional, but may be useful for type definitions or if you extend the application to send replies using the Sinch SDK. Not strictly required for receiving and verifying webhooks as shown in this guide.
1.3 Set up Prisma
Initialize Prisma in your project:
npx prisma init
This creates a prisma
directory with a schema.prisma
file and a .env
file at the project root.
1.4 Configure Environment Variables
Open the .env
file created by Prisma (or create one if it doesn't exist) and add the following variables. Do not commit this file to version control.
# .env
# Database Connection (Replace with your actual credentials)
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
# NextAuth Secret (Generate using: openssl rand -base64 32)
AUTH_SECRET=YOUR_AUTH_SECRET_GENERATED_WITH_OPENSSL
# Sinch Credentials (Get these from your Sinch Dashboard)
# Needed primarily if using the SDK to send messages.
SINCH_PROJECT_ID=YOUR_SINCH_PROJECT_ID
SINCH_APP_KEY=YOUR_SINCH_APP_KEY
SINCH_APP_SECRET=YOUR_SINCH_APP_SECRET
# Sinch Webhook Verification (Create a strong secret)
# This is a secret *you* define and configure in the Sinch portal for webhook signature verification.
# Generate using: openssl rand -base64 32
SINCH_WEBHOOK_SECRET=YOUR_STRONG_RANDOM_WEBHOOK_SECRET
# Base URL for your application (Needed for NextAuth, especially in production)
# Development: http://localhost:3000
# Production: https://yourdomain.com
NEXTAUTH_URL=http://localhost:3000
DATABASE_URL
: Your full PostgreSQL connection string.AUTH_SECRET
: Generate a strong secret usingopenssl rand -base64 32
in your terminal. Crucial for NextAuth session security.SINCH_PROJECT_ID
,SINCH_APP_KEY
,SINCH_APP_SECRET
: Obtain these from your Sinch account dashboard. Primarily needed if you plan to send outbound messages via the SDK.SINCH_WEBHOOK_SECRET
: This is a secret you create. It needs to be securely generated (e.g., usingopenssl rand -base64 32
) and then configured in the Sinch dashboard for your webhook endpoint. Sinch will use this secret to sign incoming webhook requests.NEXTAUTH_URL
: The canonical URL of your application. Essential for redirects and callbacks.
1.5 Define Database Schema
Open prisma/schema.prisma
and define your models. We assume phone numbers will be stored and handled in E.164 format (e.g., +15551234567
) for consistency.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique // Index implied by @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique // Index implied by @unique
emailVerified DateTime?
password String? // Store hashed password
phoneNumber String? @unique // Store user's phone number (E.164 format), index implied by @unique
image String?
accounts Account[]
sessions Session[]
messages Message[] // Relation to messages received by this user
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Message {
id String @id @default(cuid())
sinchMessageId String @unique // ID from Sinch, index implied by @unique
fromNumber String // Sender's phone number (E.164 format expected from Sinch)
toNumber String // Your Sinch number (E.164 format)
body String? @db.Text // Message content
direction String // 'inbound' or 'outbound'
timestamp DateTime // Timestamp from Sinch or when received
userId String? // Link to the User who received/sent the message
user User? @relation(fields: [userId], references: [id], onDelete: SetNull) // Optional link
createdAt DateTime @default(now()) // When we stored it
@@index([userId]) // Explicit index for querying messages by user
@@index([fromNumber]) // Explicit index for querying messages by sender
}
- Added
password
andphoneNumber
(unique, E.164 format assumed) toUser
. - Created
Message
model to store SMS details, linked optionally toUser
. - Added explicit
@@index
onMessage.userId
andMessage.fromNumber
for potential query performance improvements, although@unique
on other fields already implies an index.
1.6 Apply Database Migrations
Run the following command to sync your schema with the database.
npx prisma db push
(Note: For production, use prisma migrate dev
and prisma migrate deploy
for robust migration management).
1.7 Project Structure Overview
Your src
directory should look something like this:
src/
├── app/
│ ├── api/ # API routes (Webhook handler here)
│ │ └── webhooks/
│ │ └── sinch/
│ │ └── route.ts # Sinch webhook handler
│ ├── (auth)/ # Authentication related pages (optional grouping)
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── signup/
│ │ └── page.tsx
│ ├── dashboard/ # Protected area
│ │ └── page.tsx
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── globals.css
├── components/ # Reusable UI components
│ ├── auth/
│ │ ├── LoginForm.tsx
│ │ └── SignUpForm.tsx
│ └── ui/ # UI primitives (Button, Input, Label - needs creation/install)
│ ├── button.tsx
│ ├── input.tsx
│ └── label.tsx
├── lib/ # Helper functions, Prisma client, actions
│ ├── prisma.ts # Prisma client instance
│ ├── actions/ # Server Actions (auth, potentially messaging)
│ │ └── auth.ts
│ └── utils.ts # Utility functions (e.g., webhook verification)
├── auth.config.ts # NextAuth base configuration
├── auth.ts # NextAuth main configuration & handlers
├── middleware.ts # NextAuth Middleware for route protection
└── ... (other config files)
2. Implementing Core Functionality (Authentication)
We'll set up NextAuth.js for user registration and login using email, password, and phone number.
2.1 Prisma Client Instance
Create a file to instantiate and export your Prisma client singleton.
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
// In production, only log errors. In development, log queries, errors, and warnings.
const prisma =
global.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma;
}
export default prisma;
auth.config.ts
)
2.2 NextAuth Base Configuration (This file defines basic configurations like custom pages and the authorization callback used by the middleware.
// src/auth.config.ts
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
// trustHost: true, // Consider uncommenting if deploying behind a proxy and facing issues, but understand security implications.
pages: {
signIn: '/login', // Redirect users to /login if they need to authenticate
// error: '/auth/error', // Optional: Custom error page
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
const isOnAuthPage = nextUrl.pathname === '/login' || nextUrl.pathname === '/signup';
if (isOnDashboard) {
if (isLoggedIn) return true; // Allow access if logged in
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn && isOnAuthPage) {
// If logged in and trying to access login/signup, redirect to dashboard
const dashboardUrlBase = process.env.NEXTAUTH_URL;
if (dashboardUrlBase) {
// Prefer absolute redirect if NEXTAUTH_URL is set
console.log(`Redirecting logged-in user from ${nextUrl.pathname} to dashboard.`);
return Response.redirect(new URL('/dashboard', dashboardUrlBase));
} else {
// Fallback to relative redirect if NEXTAUTH_URL is missing (less ideal)
console.warn('NEXTAUTH_URL is not set. Attempting relative redirect to /dashboard.');
// Note: Relative redirects in middleware might not work as expected in all environments.
// Consider ensuring NEXTAUTH_URL is always set.
// For this example, we attempt it, but it might require returning false to trigger default redirect.
return Response.redirect(new URL('/dashboard', nextUrl.origin)); // Try constructing absolute from origin
}
}
// Allow access to all other pages (home, public routes, auth pages if not logged in)
return true;
},
// JWT and Session callbacks are defined in auth.ts
},
providers: [
// Provider definitions will go in auth.ts
],
} satisfies NextAuthConfig;
auth.ts
)
2.3 NextAuth Main Configuration (This file includes the provider logic (Credentials), database interactions, and password handling.
// src/auth.ts
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { authConfig } from './auth.config';
import prisma from '@/lib/prisma';
import type { User } from '@prisma/client';
// Helper function to find user by email
async function getUserByEmail(email: string): Promise<User | null> {
try {
// In production, replace console.log with structured logging
// logger.info({ email }, 'Attempting to fetch user by email');
return await prisma.user.findUnique({ where: { email } });
} catch (error) {
// Log error to a monitoring service in production
console.error('Failed to fetch user by email:', error);
// logger.error({ email, error }, 'Database error while fetching user');
throw new Error('Database error while fetching user.'); // Re-throw for NextAuth to handle
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...authConfig, // Spread the base config
adapter: PrismaAdapter(prisma), // Use Prisma for session, account, etc. storage
session: { strategy: 'jwt' }, // Use JWT strategy for sessions
providers: [
Credentials({
async authorize(credentials) {
// 1. Validate credentials using Zod
const parsedCredentials = z
.object({
email: z.string().email({ message: 'Invalid email format.' }),
password: z.string().min(6, { message: 'Password must be at least 6 characters.' }),
})
.safeParse(credentials);
if (!parsedCredentials.success) {
console.log('Invalid credentials format:', parsedCredentials.error.flatten().fieldErrors);
return null; // Returning null triggers CredentialsSignin error type
}
const { email, password } = parsedCredentials.data;
// 2. Find user in the database
const user = await getUserByEmail(email);
if (!user || !user.password) {
console.log(`No user found for email: ${email} or user has no password set.`);
return null; // User not found or password not set
}
// 3. Compare passwords
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) {
console.log(`User ${email} authenticated successfully.`);
// Return the user object required by NextAuth
// Ensure the returned object matches NextAuth's expected User shape for the session/JWT
return { id: user.id, email: user.email, name: user.name, image: user.image };
}
// 4. If passwords don't match
console.log(`Password mismatch for user: ${email}`);
return null; // Invalid password
},
}),
// Add other providers like Google, GitHub, etc. here if needed
],
callbacks: {
...authConfig.callbacks, // Include callbacks from authConfig (like `authorized`)
async jwt({ token, user }) {
// Add custom claims to JWT (e.g., user ID) after sign in
if (user) {
token.id = user.id;
// You could add roles or other data here if needed
// token.role = user.role; // Assuming user object has role
}
return token;
},
async session({ session, token }) {
// Add custom data to the session object from the JWT
if (token && session.user) {
session.user.id = token.id as string; // Add user ID to session.user
// session.user.role = token.role; // Add role if present in token
}
return session;
},
}
});
2.4 Middleware for Route Protection
Create the middleware file to protect routes based on authentication status using the authorized
callback defined in auth.config.ts
.
// src/middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
// Initialize NextAuth using the configuration that contains the 'authorized' callback
export default NextAuth(authConfig).auth;
export const config = {
// Matcher specifies routes where the middleware should run.
// It uses a negative lookahead to exclude API routes, static files, images, etc.
// Adjust the matcher if you have other top-level routes to exclude or include.
// Example: ['/dashboard/:path*', '/settings']
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$|favicon.ico).*)'],
};
actions/auth.ts
)
2.5 Server Actions for Auth (Create Server Actions for handling sign-up and login form submissions.
// src/lib/actions/auth.ts
'use server';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import { AuthError } from 'next-auth';
import { signIn, signOut } from '@/auth'; // Import signIn/signOut from auth.ts
import prisma from '@/lib/prisma';
// --- Login Action ---
const LoginSchema = z.object({
email: z.string().email({ message: 'Please enter a valid email.' }),
password: z.string().min(1, { message: 'Password is required.' }), // Basic check, NextAuth authorize does deeper check
});
export type LoginState = {
errors?: {
email?: string[];
password?: string[];
credentials?: string[]; // For general sign-in errors
};
message?: string | null;
};
export async function authenticate(
prevState: LoginState | undefined,
formData: FormData,
): Promise<LoginState> {
const validatedFields = LoginSchema.safeParse(
Object.fromEntries(formData.entries()),
);
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Invalid fields. Failed to Login.',
};
}
const { email, password } = validatedFields.data;
try {
// In production, replace console.log with structured logging
console.log(`Attempting sign in for user: ${email}`);
await signIn('credentials', {
email,
password,
redirect: false, // Handle redirect via middleware or explicitly on success return
});
// If signIn succeeds without throwing, middleware will likely handle redirect.
console.log(`Sign in successful for: ${email}. Redirect should occur via middleware.`);
// Return success state, though middleware might redirect before this is processed client-side
return { message: 'Login successful! Redirecting...' };
} catch (error) {
if (error instanceof AuthError) {
// Log specific AuthError types for debugging
switch (error.type) {
case 'CredentialsSignin':
console.error('CredentialsSignin error:', error.cause?.err?.message);
return { message: 'Invalid email or password.', errors: { credentials: ['Invalid email or password.'] } };
case 'CallbackRouteError':
console.error('CallbackRouteError:', error.cause?.err?.message);
return { message: `Authentication callback failed: ${error.cause?.err?.message || 'Unknown callback error'}`, errors: { credentials: ['Authentication callback failed.'] } };
default:
console.error('Unknown NextAuth error:', error);
return { message: 'Something went wrong during authentication.', errors: { credentials: ['An unexpected error occurred.'] } };
}
}
// Handle non-AuthError exceptions (e.g., database connection issues caught before signIn is called)
console.error('Non-AuthError during authenticate:', error);
// Consider re-throwing if it's a critical, unexpected error that should halt execution.
// For now, return a generic server error message.
// throw error; // Uncomment if critical errors should propagate and potentially crash the request
return { message: 'An unexpected server error occurred during login.', errors: { credentials: ['Server error during login.'] } };
}
}
// --- Sign Up Action ---
const SignUpSchema = z.object({
email: z.string().email({ message: 'Please enter a valid email.' }),
password: z.string().min(6, { message: 'Password must be at least 6 characters.' }),
name: z.string().min(1, { message: 'Name is required.' }),
// Ensure phone number matches E.164 format for consistency
phoneNumber: z.string()
.min(10, { message: 'Please enter a valid phone number (e.g., +15551234567).' })
.regex(/^\+?[1-9]\d{1,14}$/, { message: ""Invalid phone number format. Use E.164 (e.g., +15551234567)"" }),
});
export type SignUpState = {
errors?: {
email?: string[];
password?: string[];
name?: string[];
phoneNumber?: string[];
server?: string[]; // For general server/database errors
};
message?: string | null;
success?: boolean;
};
export async function registerUser(
prevState: SignUpState | undefined,
formData: FormData,
): Promise<SignUpState> {
const validatedFields = SignUpSchema.safeParse(
Object.fromEntries(formData.entries()),
);
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Invalid fields. Failed to Sign Up.',
success: false,
};
}
const { email, password, name, phoneNumber } = validatedFields.data;
// Ensure phone number starts with '+' for consistent E.164 storage, if not already present
const formattedPhoneNumber = phoneNumber.startsWith('+') ? phoneNumber : `+${phoneNumber}`;
// Check if user already exists
try {
const existingUserByEmail = await prisma.user.findUnique({ where: { email } });
if (existingUserByEmail) {
return { message: 'Email already in use.', errors: { email: ['Email already exists.'] }, success: false };
}
const existingUserByPhone = await prisma.user.findUnique({ where: { phoneNumber: formattedPhoneNumber } });
if (existingUserByPhone) {
return { message: 'Phone number already in use.', errors: { phoneNumber: ['Phone number already exists.'] }, success: false };
}
// Hash the password
const hashedPassword = await bcrypt.hash(password, 10); // Salt rounds = 10
// Create the user in the database
await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
phoneNumber: formattedPhoneNumber, // Store consistently formatted number
},
});
// In production, replace console.log with structured logging
console.log(`User registered successfully: ${email}`);
// Optionally: Automatically sign in the user after registration
// await signIn('credentials', { email, password, redirect: false });
return { message: 'Registration successful! Please log in.', success: true };
} catch (error) {
console.error('Registration error:', error);
// Log to monitoring service in production
// Check for specific Prisma unique constraint violation errors (race condition safety)
if (error instanceof Error && 'code' in error && (error as any).code === 'P2002') {
const target = (error as any).meta?.target;
if (target?.includes('email')) {
return { message: 'Email already in use.', errors: { email: ['Email already exists.'] }, success: false };
}
if (target?.includes('phoneNumber')) {
return { message: 'Phone number already in use.', errors: { phoneNumber: ['Phone number already exists.'] }, success: false };
}
}
return { message: 'Database error: Failed to register user.', errors: { server: ['Could not create user account.'] }, success: false };
}
}
// --- Sign Out Action ---
export async function logout() {
try {
// In production, replace console.log with structured logging
console.log(""Attempting user sign out."");
await signOut({ redirectTo: '/login' }); // Redirect to login after sign out
} catch (error) {
console.error(""Sign out error:"", error);
// Log error to monitoring service in production
// Handle potential errors during sign out, though typically straightforward
// Maybe redirect to an error page or show a message if signOut itself fails
// For now, the redirect might still happen client-side even if server action errors.
}
}
2.6 UI Components (Login and Sign Up Forms)
Create basic form components. Note: These examples assume you have basic Button
, Input
, and Label
components. You can create these yourself or install a library like shadcn/ui
:
npx shadcn-ui@latest init
npx shadcn-ui@latest add button input label
// src/components/ui/button.tsx, input.tsx, label.tsx
// ... Implement or install basic UI components ...
// src/components/auth/LoginForm.tsx
'use client';
import { useActionState } from 'react';
import { authenticate, LoginState } from '@/lib/actions/auth';
import { Button } from '@/components/ui/button'; // Adjust path if needed
import { Input } from '@/components/ui/input'; // Adjust path if needed
import { Label } from '@/components/ui/label'; // Adjust path if needed
import Link from 'next/link'; // Use Next.js Link for navigation
export default function LoginForm() {
const initialState: LoginState | undefined = undefined;
const [state, formAction, isPending] = useActionState(authenticate, initialState);
return (
<form action={formAction} className=""space-y-4 w-full max-w-sm mx-auto p-6 border rounded-lg shadow-md bg-white"">
<h2 className=""text-2xl font-semibold text-center"">Login</h2>
{/* Display general success or error messages */}
{state?.message && !state.errors?.credentials && !state.errors?.email && !state.errors?.password && (
<p className={`text-sm p-2 rounded ${state.errors ? 'text-red-600 bg-red-100' : 'text-green-600 bg-green-100'}`}>
{state.message}
</p>
)}
{/* Display specific credentials error */}
{state?.errors?.credentials && (
<p className=""text-sm text-red-600 bg-red-100 p-2 rounded"">{state.errors.credentials.join(', ')}</p>
)}
<div className=""space-y-1"">
<Label htmlFor=""email"">Email</Label>
<Input
id=""email""
name=""email""
type=""email""
placeholder=""you@example.com""
required
aria-describedby=""email-error""
/>
{state?.errors?.email && (
<p id=""email-error"" className=""text-sm text-red-500"">
{state.errors.email.join(', ')}
</p>
)}
</div>
<div className=""space-y-1"">
<Label htmlFor=""password"">Password</Label>
<Input
id=""password""
name=""password""
type=""password""
required
minLength={6}
aria-describedby=""password-error""
/>
{state?.errors?.password && (
<p id=""password-error"" className=""text-sm text-red-500"">
{state.errors.password.join(', ')}
</p>
)}
</div>
<Button type=""submit"" className=""w-full"" aria-disabled={isPending}>
{isPending ? 'Logging in...' : 'Log In'}
</Button>
<p className=""text-center text-sm text-gray-600"">
Don't have an account?{' '}
<Link href=""/signup"" className=""text-blue-600 hover:underline"">
Sign Up
</Link>
</p>
</form>
);
}
// src/components/auth/SignUpForm.tsx
'use client';
import { useActionState, useEffect } from 'react';
import { registerUser, SignUpState } from '@/lib/actions/auth';
import { Button } from '@/components/ui/button'; // Adjust path
import { Input } from '@/components/ui/input'; // Adjust path
import { Label } from '@/components/ui/label'; // Adjust path
import { useRouter } from 'next/navigation';
import Link from 'next/link'; // Use Next.js Link
export default function SignUpForm() {
const initialState: SignUpState | undefined = undefined;
const [state, formAction, isPending] = useActionState(registerUser, initialState);
const router = useRouter();
// Redirect on successful registration
useEffect(() => {
if (state?.success) {
// Consider showing a success toast/notification here instead of alert
console.log(""Registration successful:"", state.message);
// Redirect to login page after successful registration
router.push('/login');
}
}, [state?.success, state?.message, router]);
return (
<form action={formAction} className=""space-y-4 w-full max-w-sm mx-auto p-6 border rounded-lg shadow-md bg-white"">
<h2 className=""text-2xl font-semibold text-center"">Sign Up</h2>
{/* Display general error messages (not success message, as we redirect) */}
{state?.message && !state.success && (
<p className=""text-sm text-red-600 bg-red-100 p-2 rounded"">{state.message}</p>
)}
{state?.errors?.server && (
<p className=""text-sm text-red-600 bg-red-100 p-2 rounded"">{state.errors.server.join(', ')}</p>
)}
<div className=""space-y-1"">
<Label htmlFor=""name"">Name</Label>
{/* ... (rest of SignUpForm component - input fields for name, email, phone, password) ... */}
{/* Make sure inputs have correct id, name, type, required attributes, and error handling similar to LoginForm */}
{/* Example for Name input: */}
<Input
id=""name""
name=""name""
type=""text""
placeholder=""Your Name""
required
aria-describedby=""name-error""
/>
{state?.errors?.name && (
<p id=""name-error"" className=""text-sm text-red-500"">
{state.errors.name.join(', ')}
</p>
)}
</div>
{/* Add similar divs for Email, Phone Number, and Password */}
<div className=""space-y-1"">
<Label htmlFor=""email"">Email</Label>
<Input
id=""email""
name=""email""
type=""email""
placeholder=""you@example.com""
required
aria-describedby=""signup-email-error""
/>
{state?.errors?.email && (
<p id=""signup-email-error"" className=""text-sm text-red-500"">
{state.errors.email.join(', ')}
</p>
)}
</div>
<div className=""space-y-1"">
<Label htmlFor=""phoneNumber"">Phone Number (e.g., +15551234567)</Label>
<Input
id=""phoneNumber""
name=""phoneNumber""
type=""tel"" // Use type=""tel"" for phone numbers
placeholder=""+15551234567""
required
aria-describedby=""phoneNumber-error""
/>
{state?.errors?.phoneNumber && (
<p id=""phoneNumber-error"" className=""text-sm text-red-500"">
{state.errors.phoneNumber.join(', ')}
</p>
)}
</div>
<div className=""space-y-1"">
<Label htmlFor=""signup-password"">Password</Label>
<Input
id=""signup-password""
name=""password""
type=""password""
required
minLength={6}
aria-describedby=""signup-password-error""
/>
{state?.errors?.password && (
<p id=""signup-password-error"" className=""text-sm text-red-500"">
{state.errors.password.join(', ')}
</p>
)}
</div>
<Button type=""submit"" className=""w-full"" aria-disabled={isPending}>
{isPending ? 'Signing Up...' : 'Sign Up'}
</Button>
<p className=""text-center text-sm text-gray-600"">
Already have an account?{' '}
<Link href=""/login"" className=""text-blue-600 hover:underline"">
Log In
</Link>
</p>
</form>
);
}