npm package: @splendidlabz/auth
Description: A comprehensive authentication system built specifically for Astro applications. This package provides both server-side and client-side authentication components with support for database sessions or stateless cookie-based authentication.
Author: Zell Liew
Latest version: 1.1.2

# Auth Package

A comprehensive authentication system built specifically for Astro applications. This package provides both server-side and client-side authentication components with support for database sessions or stateless cookie-based authentication.

## Installation

```bash
npm install @splendidlabz/auth
```

For Drizzle ORM support, also install:

```bash
npm install drizzle-orm
# Choose your database driver:
npm install better-sqlite3  # For SQLite
npm install pg              # For PostgreSQL
npm install mysql2          # For MySQL
```

## Basic Usage

This auth package supports two authentication strategies and two database options:

### Authentication Strategies

#### 1. Cookie Strategy (Stateless)

Uses signed cookies to store session data. No database sessions required.

```js
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { db, User } from 'astro:db'

export const auth = splAuth({
  db, // Still needed for user lookup
  tables: { User }, // Only User table required
  auth: {
    strategy: 'cookie',
    secret: process.env.AUTH_SECRET, // Required for cookie signing
    userAttributes: ['email', 'first_name', 'name'],
    cookieOptions: {
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
    },
  },
})
```

#### 2. Database Strategy (Stateful)

Stores sessions in the database for centralized session management.

```js
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { db, User, Session, PasswordResetToken } from 'astro:db'

export const auth = splAuth({
  db,
  tables: { User, Session, PasswordResetToken },
  auth: {
    strategy: 'db',
    userAttributes: ['email', 'first_name', 'name'],
  },
})
```

### Database Options

#### Option 1: Using Astro DB (Recommended)

```js
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { db, User, Session, PasswordResetToken } from 'astro:db'

export const auth = splAuth({
  db,
  tables: { User, Session, PasswordResetToken },
  auth: {
    strategy: 'db', // or 'cookie'
    userAttributes: ['email', 'first_name', 'name'],
  },
})
```

#### Option 2: Using Drizzle ORM

```js
// src/services/auth.js
import { auth as splAuth } from '@splendidlabz/auth'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { users, sessions, passwordResetTokens } from './db/schema.js'

const sqlite = new Database('auth.db')
const db = drizzle(sqlite)

export const auth = splAuth({
  db,
  tables: {
    User: users,
    Session: sessions, // Only for 'db' strategy
    PasswordResetToken: passwordResetTokens,
  },
  auth: {
    strategy: 'db', // or 'cookie'
    userAttributes: ['email', 'firstName', 'name'],
  },
})
```

### Web/Client-side Setup

```js
// src/services/web/auth.js
import { webAuth as wa } from '@splendidlabz/auth/web'

export const webAuth = wa({
  tokenName: 'auth_token',
  authEndpoint: '/api/auth/authenticate/',
})
```

### Basic Implementation

#### 1. Login Page

```astro
---
// src/pages/auth/login.astro
import { auth } from '@/services/auth.js'
import { parseData } from '@splendidlabz/astro/server'

const redirectTo = Astro.url.searchParams.get('redirect_to')
export const prerender = false

let error

if (Astro.request.method === 'POST') {
  const body = await parseData(Astro)
  const { email, password } = body

  try {
    await auth.login(Astro, { email, password })
    return Astro.redirect(redirectTo || '/dashboard/')
  } catch (e) {
    error = e
  }
}
---

<h1>Login</h1>
<form method="POST">
  <input type="email" name="email" placeholder="Email" required />
  <input type="password" name="password" placeholder="Password" required />
  <button type="submit">Log in</button>
</form>
{error && <p class="error">{error.message}</p>}
```

#### 2. Middleware Protection

```js
// src/middleware/index.js
import { auth } from '@/services/auth.js'
import { AuthManager } from '@splendidlabz/utils'
import { sequence } from 'astro:middleware'

export const onRequest = sequence(authenticate)

const { isProtectedRoute } = AuthManager({
  protectedRoutes: {
    is: ['/dashboard/'],
    startsWith: ['/account'],
  },
})

async function authenticate(context, next) {
  if (!isProtectedRoute(context.url.pathname)) return next()

  try {
    await auth.authenticate(context)
    return next()
  } catch (e) {
    const originalPathname = context.url.pathname
    return context.redirect(`/auth/login/?redirect_to=${originalPathname}`)
  }
}
```

#### 3. API Routes

```js
// src/pages/api/auth/authenticate.js
import { auth } from '@/services/auth.js'

export async function POST(context) {
  try {
    const result = await auth.authenticate(context)
    return new Response(JSON.stringify(result), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    })
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    })
  }
}
```

## Config

### Strategy Comparison

| Feature                  | Database Strategy                        | Cookie Strategy             |
| ------------------------ | ---------------------------------------- | --------------------------- |
| **Session Storage**      | Database tables                          | Signed cookies              |
| **State**                | Stateful                                 | Stateless                   |
| **Database Requirement** | User, Session, PasswordResetToken tables | Only User table             |
| **Server Memory**        | Low                                      | Very low                    |
| **Security**             | Session invalidation possible            | Relies on cookie expiration |
| **Logout**               | Immediate (removes from DB)              | Client-side only            |
| **Session Management**   | Centralized                              | Distributed                 |
| **Best For**             | Multi-device apps, admin panels          | Simple apps, APIs           |

**Use Database Strategy when:**

- You need immediate session invalidation
- Building multi-device applications
- Want centralized session management
- Need to track active sessions

**Use Cookie Strategy when:**

- Building stateless applications
- Want minimal database overhead
- Working with microservices/APIs
- Don't need session tracking

### Configuration Options

#### Server Auth Configuration

```js
export const auth = splAuth({
  db, // Database instance (required)
  tables: {}, // Database tables (required)
  auth: {
    strategy, // 'db' or 'cookie' (required)
    secret, // Required for 'cookie' strategy
    userAttributes, // Array of user fields to include in session
    cookieOptions, // Cookie settings (cookie strategy only)
    additionalVerification, // Custom verification function
  },
})
```

**Parameters:**

- **db**: Your database instance (Astro DB or Drizzle)
- **tables**: Object mapping table names to table schemas
- **auth.strategy**: `'db'` for database sessions, `'cookie'` for stateless
- **auth.secret**: Secret key for cookie signing (required for cookie strategy)
- **auth.userAttributes**: Array of user fields to include in session data
- **auth.cookieOptions**: Cookie configuration object (cookie strategy only)
- **auth.additionalVerification**: Optional async function for custom verification

#### Cookie Options (Cookie Strategy Only)

```js
cookieOptions: {
  maxAge: 7 * 24 * 60 * 60 * 1000, // Cookie lifetime in milliseconds
  httpOnly: true,                   // Prevent client-side access
  secure: true,                     // HTTPS only in production
  sameSite: 'lax',                 // CSRF protection
}
```

#### Web Auth Configuration

```js
export const webAuth = wa({
  tokenName: 'auth_token', // Token name in storage
  authEndpoint: '/api/auth/authenticate/', // Authentication API endpoint
})
```

### Database Schema

#### Astro DB Schema

```js
// db/config.ts
import { defineDb, defineTable, column } from 'astro:db'

const User = defineTable({
  columns: {
    id: column.text({ primaryKey: true }),
    email: column.text({ unique: true }),
    password: column.text(),
    first_name: column.text({ optional: true }),
    name: column.text({ optional: true }),
    created_at: column.date({ default: new Date() }),
  },
})

// Only needed for 'db' strategy
const Session = defineTable({
  columns: {
    id: column.text({ primaryKey: true }),
    user_id: column.text({ references: () => User.columns.id }),
    expires_at: column.date(),
    created_at: column.date({ default: new Date() }),
  },
})

const PasswordResetToken = defineTable({
  columns: {
    id: column.text({ primaryKey: true }),
    user_id: column.text({ references: () => User.columns.id }),
    token: column.text({ unique: true }),
    expires_at: column.date(),
    created_at: column.date({ default: new Date() }),
  },
})

export default defineDb({
  tables: { User, Session, PasswordResetToken },
})
```

#### Drizzle Schema (SQLite)

```js
// src/db/schema.js
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'

export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  email: text('email').unique().notNull(),
  password: text('password').notNull(),
  firstName: text('first_name'),
  name: text('name'),
  createdAt: integer('created_at', { mode: 'timestamp' }).default(
    sql`CURRENT_TIMESTAMP`,
  ),
})

// Only needed for 'db' strategy
export const sessions = sqliteTable('sessions', {
  id: text('id').primaryKey(),
  userId: text('user_id')
    .notNull()
    .references(() => users.id),
  expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).default(
    sql`CURRENT_TIMESTAMP`,
  ),
})

export const passwordResetTokens = sqliteTable('password_reset_tokens', {
  id: text('id').primaryKey(),
  userId: text('user_id')
    .notNull()
    .references(() => users.id),
  token: text('token').unique().notNull(),
  expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).default(
    sql`CURRENT_TIMESTAMP`,
  ),
})
```

#### Drizzle Schema (PostgreSQL)

```js
// src/db/schema.js
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'

export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  email: text('email').unique().notNull(),
  password: text('password').notNull(),
  firstName: text('first_name'),
  name: text('name'),
  createdAt: timestamp('created_at').default(sql`CURRENT_TIMESTAMP`),
})

// Only needed for 'db' strategy
export const sessions = pgTable('sessions', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id')
    .notNull()
    .references(() => users.id),
  expiresAt: timestamp('expires_at').notNull(),
  createdAt: timestamp('created_at').default(sql`CURRENT_TIMESTAMP`),
})

export const passwordResetTokens = pgTable('password_reset_tokens', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id')
    .notNull()
    .references(() => users.id),
  token: text('token').unique().notNull(),
  expiresAt: timestamp('expires_at').notNull(),
  createdAt: timestamp('created_at').default(sql`CURRENT_TIMESTAMP`),
})
```

### Complete Drizzle Setup

#### 1. Install Dependencies

```bash
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
```

#### 2. Database Configuration

```js
// src/db/index.js
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from './schema.js'

const sqlite = new Database('auth.db')
export const db = drizzle(sqlite, { schema })
```

#### 3. Drizzle Configuration

```js
// drizzle.config.js
export default {
  schema: './src/db/schema.js',
  out: './drizzle',
  driver: 'better-sqlite',
  dbCredentials: {
    url: './auth.db',
  },
}
```

#### 4. Generate Migrations

```bash
# Generate migration
npx drizzle-kit generate:sqlite

# Run migration
npx drizzle-kit push:sqlite
```

### Environment Variables

```env
# .env
DATABASE_URL=your_database_url
AUTH_SECRET=your_secret_key_for_cookie_signing

# For cookie strategy, AUTH_SECRET is required
# Generate a strong secret: openssl rand -base64 32
```

### Server Methods

#### auth.login(context, credentials)

```js
await auth.login(Astro, { email: 'user@example.com', password: 'password' })
```

#### auth.authenticate(context)

```js
const user = await auth.authenticate(Astro)
```

#### auth.logout(context)

```js
await auth.logout(Astro)
```

#### auth.createUser(userData)

```js
const user = await auth.createUser({
  email: 'user@example.com',
  password: 'password',
  first_name: 'John',
  name: 'John Doe',
})
```

### Web/Client Methods

#### webAuth.isAuthenticated()

```js
const isAuth = await webAuth.isAuthenticated()
```

#### webAuth.getUser()

```js
const user = await webAuth.getUser()
```

#### webAuth.logout()

```js
await webAuth.logout()
```

### Security Notes

#### CSRF Protection

CSRF protection is enabled by default in Astro v5.0:

```js
// astro.config.mjs
export default defineConfig({
  output: 'server',
  security: { checkOrigin: true }, // Default in Astro 5.0
})
```

#### Advanced Verification

Add custom verification logic:

```js
export const auth = splAuth({
  // ... other config
  auth: {
    strategy: 'db',
    userAttributes: ['email', 'first_name', 'name'],
    async additionalVerification(context, { user }) {
      // Check subscriptions, permissions, etc.
      const subscriptions = await getUserSubscriptions(user.id)
      const permissions = await getUserPermissions(user.id)

      return {
        subscriptions,
        permissions,
        product_access: subscriptions.length > 0,
      }
    },
  },
})
```

### Troubleshooting

#### Database Deployment with Astro DB

When using `astro:db`, you cannot use `astro db execute <file-path> --remote` directly. Instead:

1. Run `npx astro dev --remote`
2. Create an Astro page with your database operations
3. Navigate to that page once to execute the operations