Skip to content

07 - Authentication

Overview

Wavic WebApp uses NextAuth.js v4 for authentication, supporting:

  • Email/Password credentials
  • Google OAuth
  • JWT-based sessions

Architecture

┌──────────────────────────────────────────────────────────────────┐
│                      AUTHENTICATION FLOW                         │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────┐      ┌─────────────┐      ┌──────────────┐   │
│   │   Sign In   │─────►│  NextAuth   │─────►│   API        │   │
│   │   Page      │      │   Handler   │      │   /user/     │   │
│   └─────────────┘      └──────┬──────┘      └──────────────┘   │
│                               │                                  │
│                               ▼                                  │
│                        ┌─────────────┐                          │
│                        │   JWT Token │                          │
│                        │   + Session │                          │
│                        └──────┬──────┘                          │
│                               │                                  │
│                               ▼                                  │
│   ┌──────────────────────────────────────────────────────────┐ │
│   │                    Protected Routes                       │ │
│   │   (middleware.ts enforces authentication)                 │ │
│   └──────────────────────────────────────────────────────────┘ │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Configuration Files

Auth Options

typescript
// src/app/api/auth/[...nextauth]/auth-options.ts

import { type NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';

export const authOptions: NextAuthOptions = {
  pages: {
    signIn: '/signin',
    error: '/signin',
  },
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  callbacks: {
    async signIn({ user, account }) {
      if (account?.provider === 'google') {
        // Validate with backend
        const res = await fetch(
          `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/auth/google/callback?token=${account?.id_token}`
        );
        const dbUser = await res.json();
        return !!dbUser?.user;
      }
      return true;
    },

    async jwt({ token, user, account, trigger, session }) {
      if (account?.provider === 'credentials') {
        if (user) return { ...token, ...user };
      }
      if (account?.provider === 'google') {
        const res = await fetch(
          `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/auth/google/callback?token=${account?.id_token}`
        );
        const dbUser = await res.json();
        if (dbUser) return { token: dbUser.token, user: dbUser.user };
      }
      if (trigger === 'update') {
        return { ...token, ...session };
      }
      return token;
    },

    async session({ session, token }) {
      if (token.user) session.user = token.user;
      if (token.token) session.token = token.token;
      if (token.iat) session.iat = token.iat;
      if (token.exp) session.exp = token.exp;
      return session;
    },
  },
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const res = await fetch(
          `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/signin`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials),
          }
        );
        const data = await res.json();
        if (data.token && data.user) {
          return { token: data.token, user: data.user };
        }
        return null;
      },
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
};

Pages Options

typescript
// src/app/api/auth/[...nextauth]/pages-options.ts

export const pagesOptions = {
  signIn: '/signin',
  error: '/signin',
};

Auth Provider

typescript
// src/app/api/auth/[...nextauth]/auth-provider.tsx
'use client';

import { SessionProvider } from 'next-auth/react';

export default function AuthProvider({
  children,
  session
}: {
  children: React.ReactNode;
  session: any;
}) {
  return (
    <SessionProvider session={session}>
      {children}
    </SessionProvider>
  );
}

Middleware (Route Protection)

typescript
// src/middleware.ts

import { pagesOptions } from '@/app/api/auth/[...nextauth]/pages-options';
import withAuth from 'next-auth/middleware';

export default withAuth({
  pages: {
    ...pagesOptions,
  },
});

export const config = {
  // Protected routes
  matcher: [
    '/',
    '/artist/:path*',
    '/project/:path*',
    '/wave/:path*',
    '/profile',
    '/blank',
    '/transfers',
    '/library',
    '/storage',
    '/user/:path*',
    '/trash',
    '/onboarding',
  ],
};

Session Type Extensions

typescript
// types/next-auth.d.ts

import 'next-auth';
import { IUser } from './user';

declare module 'next-auth' {
  interface Session {
    user: IUser;
    token: string;
    iat: number;
    exp: number;
    jti: string;
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    user: IUser;
    token: string;
  }
}

Using Authentication

In Server Components

typescript
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options';

export default async function Page() {
  const session = await getServerSession(authOptions);

  if (!session) {
    redirect('/signin');
  }

  return <Dashboard user={session.user} />;
}

In Client Components

typescript
'use client';

import { useSession, signIn, signOut } from 'next-auth/react';

function UserMenu() {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return <Loader />;
  }

  if (status === 'unauthenticated') {
    return <Button onClick={() => signIn()}>Sign In</Button>;
  }

  return (
    <div>
      <span>{session.user.fullName}</span>
      <Button onClick={() => signOut()}>Sign Out</Button>
    </div>
  );
}

Getting Token for API Calls

typescript
'use client';

import { useSession } from 'next-auth/react';

function MyComponent() {
  const { data: session } = useSession();

  const fetchData = async () => {
    const response = await fetch('/api/data', {
      headers: {
        Authorization: `Bearer ${session?.token}`,
      },
    });
    return response.json();
  };

  // ...
}

Sign In Page

typescript
// src/app/signin/page.tsx

'use client';

import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function SignInPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const result = await signIn('credentials', {
      email,
      password,
      redirect: false,
    });

    if (result?.error) {
      setError('Invalid credentials');
    } else {
      router.push('/');
    }
  };

  const handleGoogleSignIn = () => {
    signIn('google', { callbackUrl: '/' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      {error && <p className="text-red-500">{error}</p>}
      <button type="submit">Sign In</button>
      <button type="button" onClick={handleGoogleSignIn}>
        Sign in with Google
      </button>
    </form>
  );
}

Session Updates

To update session data (e.g., after profile changes):

typescript
import { useSession } from 'next-auth/react';

function ProfileForm() {
  const { update } = useSession();

  const handleProfileUpdate = async (newData) => {
    // Update on server
    await updateProfile(newData);

    // Update session
    await update({
      user: {
        ...session.user,
        ...newData,
      },
    });
  };
}

Environment Variables

bash
# Required for NextAuth
NEXTAUTH_SECRET=your-secret-key-min-32-chars
NEXTAUTH_URL=http://localhost:3000

# Google OAuth (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret

# API
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080

Authentication Flow

Email/Password Sign In

  1. User enters credentials on /signin
  2. signIn('credentials', {...}) called
  3. NextAuth calls authorize() callback
  4. Backend validates at /user/signin
  5. Backend returns JWT + user data
  6. NextAuth creates session
  7. User redirected to protected route

Google OAuth Sign In

  1. User clicks "Sign in with Google"
  2. signIn('google') called
  3. User redirected to Google consent screen
  4. Google redirects back with token
  5. NextAuth signIn callback validates with backend
  6. Backend at /user/auth/google/callback creates/finds user
  7. Session created, user redirected

Security Considerations

  1. JWT Secret: Use strong, random secret for NEXTAUTH_SECRET
  2. HTTPS: Always use HTTPS in production
  3. Token Expiry: Sessions expire after 30 days
  4. Protected Routes: Middleware enforces authentication
  5. API Authorization: All API calls include Bearer token

Ctrl-Audio Platform Documentation