npm package: @lenan-soft/auth
Description: Reusable JWT authentication library for NestJS and React/React Native
Author: Simon Gatenby
Latest version: 1.0.7

# @lenan-soft/auth

A reusable JWT authentication library for NestJS and React/React Native applications.

## Features

- ✅ JWT access + refresh token authentication
- ✅ NestJS dynamic module with guards, strategies, and decorators
- ✅ React hooks and context provider
- ✅ React Native support with Expo SecureStore adapter
- ✅ Interface-driven design — bring your own user entity
- ✅ TypeScript first with full type definitions
- ✅ Automatic token refresh on 401 responses
- ✅ Bcrypt password hashing

## Installation

```bash
npm install @lenan-soft/auth
```

### Peer Dependencies

**For NestJS:**

```bash
npm install @nestjs/common @nestjs/core @nestjs/jwt @nestjs/passport passport passport-jwt class-validator class-transformer reflect-metadata
```

**For React:**

```bash
npm install react
```

**For React Native (Expo):**

```bash
npx expo install expo-secure-store
```

## Quick Start

### NestJS Backend

#### 1. Implement the User Service Interface

Create a service that implements `UserServiceInterface`:

```typescript
// user.service.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { UserServiceInterface, AuthUser } from "@lenan-soft/auth/nestjs";
import { User } from "./user.entity";

@Injectable()
export class UserService implements UserServiceInterface<User> {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async findByEmail(email: string): Promise<User | null> {
    return this.userRepository.findOne({ where: { email } });
  }

  async findById(id: string): Promise<User | null> {
    return this.userRepository.findOne({ where: { id } });
  }

  async createUser(data: {
    email: string;
    passwordHash: string;
  }): Promise<User> {
    const user = this.userRepository.create({
      email: data.email,
      passwordHash: data.passwordHash,
      hashedRefreshToken: null,
    });
    return this.userRepository.save(user);
  }

  async updateRefreshToken(
    userId: string,
    hashedToken: string | null,
  ): Promise<void> {
    await this.userRepository.update(userId, {
      hashedRefreshToken: hashedToken,
    });
  }

  async validateRefreshToken(
    userId: string,
    hashedToken: string,
  ): Promise<boolean> {
    const user = await this.findById(userId);
    return user?.hashedRefreshToken === hashedToken;
  }
}
```

#### 2. Create Your User Entity

```typescript
// user.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
} from "typeorm";
import { AuthUser } from "@lenan-soft/auth/nestjs";

@Entity("users")
export class User implements AuthUser {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ unique: true })
  email!: string;

  @Column()
  passwordHash!: string;

  @Column({ nullable: true })
  hashedRefreshToken!: string | null;

  // Add your own custom fields
  @Column({ nullable: true })
  firstName?: string;

  @Column({ nullable: true })
  lastName?: string;

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;
}
```

#### 3. Register the Auth Module

```typescript
// app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { LenanAuthModule } from "@lenan-soft/auth/nestjs";
import { UserService } from "./user/user.service";
import { User } from "./user/user.entity";

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRoot({
      // your database config
    }),
    TypeOrmModule.forFeature([User]),

    // Register auth module with async configuration
    LenanAuthModule.registerAsync(
      {
        imports: [ConfigModule],
        useFactory: (config: ConfigService) => ({
          jwtSecret: config.getOrThrow("JWT_SECRET"),
          jwtAccessExpiry: config.get("JWT_ACCESS_EXPIRY", "15m"),
          jwtRefreshExpiry: config.get("JWT_REFRESH_EXPIRY", "7d"),
          jwtRefreshSecret: config.get("JWT_REFRESH_SECRET"),
        }),
        inject: [ConfigService],
      },
      UserService, // Your UserService class
    ),
  ],
  providers: [UserService],
})
export class AppModule {}
```

#### 4. Protect Routes

```typescript
// profile.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common";
import { JwtAuthGuard, CurrentUser, Public } from "@lenan-soft/auth/nestjs";

@Controller("profile")
@UseGuards(JwtAuthGuard) // Protect all routes in this controller
export class ProfileController {
  @Get()
  getProfile(@CurrentUser() user: User) {
    return { user };
  }

  @Public() // This route is accessible without authentication
  @Get("public")
  getPublicInfo() {
    return { message: "This is public" };
  }
}
```

#### 5. Apply Guard Globally (Optional)

```typescript
// main.ts
import { NestFactory, Reflector } from "@nestjs/core";
import { JwtAuthGuard } from "@lenan-soft/auth/nestjs";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Apply JWT guard globally
  const reflector = app.get(Reflector);
  app.useGlobalGuards(new JwtAuthGuard(reflector));

  await app.listen(3000);
}
bootstrap();
```

### React Frontend

```tsx
// App.tsx
import { AuthProvider } from "@lenan-soft/auth/react";

function App() {
  return (
    <AuthProvider baseUrl="https://api.example.com">
      <Router />
    </AuthProvider>
  );
}
```

#### Using Auth Hooks

```tsx
// LoginForm.tsx
import { useAuth } from "@lenan-soft/auth/react";

function LoginForm() {
  const { login, isLoading, error } = useAuth();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await login(email, password);
      // Redirect to dashboard
    } catch {
      // Error is available via `error` state
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Logging in..." : "Login"}
      </button>
    </form>
  );
}
```

#### Protected Routes

```tsx
// ProtectedRoute.tsx
import { Navigate } from "react-router-dom";
import { useAuth } from "@lenan-soft/auth/react";

function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useAuth();

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

  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }

  return <>{children}</>;
}
```

#### Display User Info

```tsx
// UserProfile.tsx
import { useUser } from "@lenan-soft/auth/react";

function UserProfile() {
  const user = useUser();

  if (!user) {
    return null;
  }

  return (
    <div>
      <h2>Welcome, {user.email}!</h2>
    </div>
  );
}
```

### React Native with Expo

```tsx
// App.tsx
import { AuthProvider } from "@lenan-soft/auth/react";
import { SecureStoreAdapter } from "@lenan-soft/auth/react/native";

const tokenStorage = new SecureStoreAdapter();

export default function App() {
  return (
    <AuthProvider baseUrl="https://api.example.com" tokenStorage={tokenStorage}>
      <RootNavigator />
    </AuthProvider>
  );
}
```

## API Reference

### NestJS Module

#### LenanAuthModule

```typescript
// Sync registration
LenanAuthModule.register(options: AuthModuleOptions, userService: Type<UserServiceInterface>)

// Async registration
LenanAuthModule.registerAsync(options: AuthModuleAsyncOptions, userService: Type<UserServiceInterface>)
```

#### AuthModuleOptions

| Option             | Type      | Default                  | Description                                    |
| ------------------ | --------- | ------------------------ | ---------------------------------------------- |
| `jwtSecret`        | `string`  | required                 | Secret for signing JWT access tokens           |
| `jwtAccessExpiry`  | `string`  | `'15m'`                  | Access token expiration                        |
| `jwtRefreshExpiry` | `string`  | `'7d'`                   | Refresh token expiration                       |
| `jwtRefreshSecret` | `string`  | `jwtSecret + '_refresh'` | Separate secret for refresh tokens             |
| `useController`    | `boolean` | `true`                   | Whether to register the default AuthController |

#### UserServiceInterface

```typescript
interface UserServiceInterface<TUser extends AuthUser = AuthUser> {
  findByEmail(email: string): Promise<TUser | null>;
  findById(id: string): Promise<TUser | null>;
  createUser(data: { email: string; passwordHash: string }): Promise<TUser>;
  updateRefreshToken(userId: string, hashedToken: string | null): Promise<void>;
  validateRefreshToken(userId: string, hashedToken: string): Promise<boolean>;
}
```

#### Decorators

| Decorator            | Description                             |
| -------------------- | --------------------------------------- |
| `@CurrentUser()`     | Extract current user from request       |
| `@CurrentUser('id')` | Extract specific user property          |
| `@Public()`          | Mark route as public (no auth required) |

#### Guards

| Guard               | Description                                   |
| ------------------- | --------------------------------------------- |
| `JwtAuthGuard`      | Validates access tokens, respects `@Public()` |
| `RefreshTokenGuard` | Validates refresh tokens                      |

#### Services

| Service           | Description                                                |
| ----------------- | ---------------------------------------------------------- |
| `AuthService`     | Main auth orchestration (login, register, refresh, logout) |
| `PasswordService` | Password hashing and comparison                            |
| `JwtTokenService` | Token generation and verification                          |

### React Package

#### AuthProvider Props

| Prop                | Type                     | Default               | Description                   |
| ------------------- | ------------------------ | --------------------- | ----------------------------- |
| `baseUrl`           | `string`                 | required              | Base URL for auth API         |
| `tokenStorage`      | `TokenStorage`           | `LocalStorageAdapter` | Token storage implementation  |
| `endpoints`         | `Partial<AuthEndpoints>` | defaults              | Custom endpoint paths         |
| `headers`           | `Record<string, string>` | `{}`                  | Custom headers for requests   |
| `autoRefresh`       | `boolean`                | `true`                | Auto-refresh on mount         |
| `onAuthStateChange` | `(state) => void`        | -                     | Callback on auth state change |

#### useAuth Hook

Returns:

| Property                                      | Type                 | Description          |
| --------------------------------------------- | -------------------- | -------------------- |
| `user`                                        | `TUser \| null`      | Current user         |
| `tokens`                                      | `AuthTokens \| null` | Current tokens       |
| `isLoading`                                   | `boolean`            | Loading state        |
| `isAuthenticated`                             | `boolean`            | Auth status          |
| `error`                                       | `string \| null`     | Error message        |
| `login(email, password)`                      | `Promise<void>`      | Login function       |
| `register(email, password, confirmPassword?)` | `Promise<void>`      | Register function    |
| `logout()`                                    | `Promise<void>`      | Logout function      |
| `refreshToken()`                              | `Promise<void>`      | Manual token refresh |
| `clearError()`                                | `() => void`         | Clear error state    |
| `client`                                      | `AuthClient`         | Direct client access |

#### useUser Hook

```typescript
const user = useUser<MyUserType>();
// Returns TUser | null
```

#### useIsAuthenticated Hook

```typescript
const isAuthenticated = useIsAuthenticated();
// Returns boolean (true only when authenticated AND not loading)
```

### Built-in Auth Endpoints

The default `AuthController` provides:

| Method | Endpoint         | Description                      |
| ------ | ---------------- | -------------------------------- |
| POST   | `/auth/register` | Register new user                |
| POST   | `/auth/login`    | Login with email/password        |
| POST   | `/auth/refresh`  | Refresh tokens                   |
| POST   | `/auth/logout`   | Logout (requires auth)           |
| GET    | `/auth/me`       | Get current user (requires auth) |

## Token Storage Adapters

### LocalStorageAdapter (Web)

Default for web browsers. Falls back to memory storage if localStorage is unavailable.

```typescript
import { LocalStorageAdapter } from "@lenan-soft/auth/react";
const storage = new LocalStorageAdapter();
```

### MemoryStorageAdapter (Testing/SSR)

In-memory storage for testing or server-side rendering.

```typescript
import { MemoryStorageAdapter } from "@lenan-soft/auth/react";
const storage = new MemoryStorageAdapter();
```

### SecureStoreAdapter (React Native)

Secure storage using Expo SecureStore.

```typescript
import { SecureStoreAdapter } from "@lenan-soft/auth/react/native";
const storage = new SecureStoreAdapter();
```

### Custom Storage

Implement the `TokenStorage` interface:

```typescript
import type { TokenStorage } from "@lenan-soft/auth/shared";

class MyCustomStorage implements TokenStorage {
  async getItem(key: string): Promise<string | null> {
    // Your implementation
  }

  async setItem(key: string, value: string): Promise<void> {
    // Your implementation
  }

  async removeItem(key: string): Promise<void> {
    // Your implementation
  }
}
```

## Extending the User Entity

The library uses an interface-driven approach. Your user entity must implement `AuthUser`:

```typescript
interface AuthUser {
  id: string;
  email: string;
  passwordHash: string;
  hashedRefreshToken: string | null;
}
```

Add any additional fields you need:

```typescript
// user.entity.ts
@Entity("users")
export class User implements AuthUser {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column({ unique: true })
  email!: string;

  @Column()
  passwordHash!: string;

  @Column({ nullable: true })
  hashedRefreshToken!: string | null;

  // Your custom fields
  @Column({ nullable: true })
  avatarUrl?: string;

  @Column({ default: "user" })
  role!: "user" | "admin";

  @Column({ default: false })
  emailVerified!: boolean;
}
```

## Security Considerations

1. **JWT Secrets**: Use strong, unique secrets for production. Consider separate secrets for access and refresh tokens.

2. **Token Expiry**: Keep access tokens short-lived (15m default). Refresh tokens can be longer (7d default).

3. **HTTPS**: Always use HTTPS in production to protect tokens in transit.

4. **Refresh Token Rotation**: The library rotates refresh tokens on each refresh, invalidating the old token.

5. **Password Requirements**: Default validation requires 8+ characters with uppercase, lowercase, and numbers.

## Development

```bash
# Install dependencies
yarn install

# Build
yarn build

# Run tests
yarn test

# Run tests with coverage
yarn test:coverage

# Lint
yarn lint
```

## License

MIT