# @amaster.ai/auth-client

Authentication SDK for Amaster Platform - Complete client-side authentication solution with OAuth, session management, and permission checks.

## Features

- 🔐 **Multiple Authentication Methods**: Username/Email/Phone + Password, Verification Code, OAuth
- 🔄 **Automatic Token Refresh**: JWT token auto-refresh before expiration
- 📦 **Token Storage**: localStorage or sessionStorage with SSR support
- 🎯 **Permission System**: Role-based and permission-based access control
- 👤 **Anonymous Access**: Automatic support for anonymous users with configurable permissions
- 🔗 **OAuth Integration**: Google, GitHub, WeChat OAuth, WeChat Mini Program, and custom OAuth providers
- 📡 **Event System**: Listen to login, logout, and token events
- 💾 **Session Management**: View and revoke active sessions
- 🔒 **Type-Safe**: Full TypeScript support

## Installation

```bash
pnpm add @amaster.ai/auth-client
# or
npm install @amaster.ai/auth-client
```

## Quick Start

```typescript
import { createAuthClient } from "@amaster.ai/auth-client";

// Initialize the client (with optional callbacks)
const authClient = createAuthClient({
  onTokenExpired: () => {
    window.location.href = "/login";
  },
  onUnauthorized: () => {
    window.location.href = "/login";
  },
});

// Register a new user
await authClient.register({
  email: "user@example.com",
  password: "Password@123",
  displayName: "John Doe",
});

// Login
const result = await authClient.login({
  loginType: "email",
  email: "user@example.com",
  password: "Password@123",
});

if (result.data) {
  console.log("Logged in:", result.data.user);
}

// Get current user
const user = await authClient.getMe();

// Check permissions
if (authClient.hasPermission("user", "read")) {
  // Show user list
}
```

## Configuration

### Zero Configuration (Recommended)

The SDK works out of the box with **zero configuration**:

```typescript
const authClient = createAuthClient();

// Storage auto-detects environment:
// - Browser → localStorage
// - WeChat Mini Program → wx.setStorageSync
// - SSR/Node.js → no-op (no persistence)
```

### Optional Configuration

```typescript
interface AuthClientOptions {
  baseURL?: string; // API base URL, defaults to window.location.origin
  headers?: Record<string, string>; // Custom headers for all requests
  onTokenExpired?: () => void; // Token expiration callback
  onUnauthorized?: () => void; // Unauthorized (401) callback
  autoHandleOAuthCallback?: boolean; // Auto-handle OAuth callback hash on init, default true
  autoRedirectAfterLogin?: boolean; // Auto-consume ?redirect=... after any successful login, default true
}
```

**Example:**

```typescript
const authClient = createAuthClient({
  onTokenExpired: () => (window.location.href = "/login"),
  onUnauthorized: () => alert("Session expired"),
});
```

**Built-in Features (Zero Config):**

- ✅ Auto environment detection (browser, WeChat Mini Program, SSR)
- ✅ Auto token refresh (5 minutes before expiry)
- ✅ Auto permission sync
- ✅ Auto anonymous support
- ✅ Smart storage (localStorage, wx.storage, no-op for SSR)

## Authentication

### Register

```typescript
await authClient.register({
  email: "user@example.com",
  password: "Password@123",
  displayName: "John Doe",
});
```

**With Captcha:**

```typescript
// 1. Get captcha
const captchaData = await authClient.getCaptcha();
document.getElementById("captcha-img").src = captchaData.data.captchaImage;

// 2. Register with captcha
const userInputCode = "AB12";
await authClient.register({
  email: "user@example.com",
  password: "Password@123",
  captcha: `${captchaData.data.captchaId}:${userInputCode}`,
});
```

### Login

**Password Login:**

```typescript
await authClient.login({
  loginType: "email",
  email: "user@example.com",
  password: "Password@123",
});
```

**Verification Code Login:**

```typescript
// 1. Send code
await authClient.sendCode({
  type: "email",
  email: "user@example.com",
});

// 2. Login with code
await authClient.loginWithCode({
  loginType: "email",
  email: "user@example.com",
  code: "123456",
});
```

### OAuth Login

**Full-page redirect (recommended):**

```typescript
// Login page
sessionStorage.setItem("oauth_redirect", "/dashboard");
authClient.loginWithOAuth("google");

// Callback page (/auth/callback)
const { user } = await authClient.handleOAuthCallback();
const redirectUrl = sessionStorage.getItem("oauth_redirect") || "/";
window.location.href = redirectUrl;
```

If the callback page URL already carries `?redirect=...` such as
`/login?redirect=%2Fapi%2Foauth%2Fauthorize...`, the SDK will reuse that
redirect target automatically after a successful OAuth callback. Both
`#access_token=...` and `#accessToken=...` callback hash formats are supported.

By default, the same `?redirect=...` handling is also applied to password login,
verification-code login, registration auto-login, and mini-program login. Set
`autoRedirectAfterLogin: false` to disable this behavior and fully control
navigation in the application layer.

When the SDK actually consumes a redirect target, successful login results will
include `result.data.redirectHandled === true` and `result.data.redirectTarget`.
Applications that still run their own post-login navigation should short-circuit
on that flag to avoid double redirects.

**Popup window:**

```typescript
// Main page
const popup = window.open("/api/auth/oauth/google", "oauth-login", "width=600,height=700");

window.addEventListener("message", (event) => {
  if (event.data.type === "oauth-success") {
    authClient.setAccessToken(event.data.accessToken);
    popup?.close();
  }
});

// Callback page
if (window.opener) {
  const { accessToken } = await authClient.handleOAuthCallback();
  window.opener.postMessage({ type: "oauth-success", accessToken }, origin);
}
```

### Mini Program Login

The SDK exposes a unified Mini Program API for both WeChat and Douyin.

- WeChat runtime: auto-detects `wx` and uses WeChat endpoints
- Douyin Mini Program: auto-detects the `tt` runtime and uses Douyin endpoints
- Non-runtime or mixed environments: you can explicitly pass `platform`

The SDK automatically detects the Mini Program storage runtime and uses WeChat `wx.storage` or Douyin `tt.storage`.
For most app code, prefer the high-level methods:

- `miniLogin()` - automatically calls `Taro.login` / Douyin `tt.login` / WeChat `wx.login`, then completes backend login
- `miniGetPhone(...)` - accepts a raw code or event object, extracts the phone code, then binds the phone number

```typescript
import { createAuthClient } from "@amaster.ai/auth-client";
import Taro from "@tarojs/taro";

// Zero configuration - auto-detects mini-program environment
const authClient = createAuthClient();

const result = await authClient.miniLogin();

if (result.data) {
  console.log("Logged in:", result.data.user);
  // Token automatically saved to mini-program storage
  Taro.switchTab({ url: "/pages/index/index" });
} else if (result.error) {
  Taro.showToast({
    title: result.error.message || "Login failed",
    icon: "none",
  });
}
```

**Login with Douyin Mini Program:**

```typescript
const result = await authClient.miniLogin({
  platform: "douyin",
});

if (result.data) {
  console.log("Logged in:", result.data.user);
} else if (result.error) {
  Taro.showToast({
    title: result.error.message || "Login failed",
    icon: "none",
  });
}
```

**Get user phone number (optional):**

```xml
<!-- WXML -->
<button
  open-type="getPhoneNumber"
  bindgetphonenumber="onGetPhoneNumber"
>
  Get Phone Number
</button>
```

```typescript
// JS
async onGetPhoneNumber(e) {
  const result = await authClient.miniGetPhone(e);

  if (result.data) {
    console.log("Phone number:", result.data.phone);
    console.log("Verified:", result.data.phoneVerified);

    // Update UI with phone number
    this.setData({
      phone: result.data.phone
    });
  } else if (result.error) {
    Taro.showToast({
      title: result.error.message || 'Failed to get phone number',
      icon: 'none'
    });
  } else {
    // User denied authorization
    console.log("User cancelled phone number authorization");
  }
}
```

**Get Douyin Mini Program phone number:**

```typescript
async function onGetPhoneNumber(e) {
  const result = await authClient.miniGetPhone({
    ...e,
    platform: "douyin",
  });

  if (result.data) {
    console.log("Phone number:", result.data.phone);
  }
}
```

**Complete Mini Program example:**

```typescript
// pages/login/login.js
import { createAuthClient } from "@amaster.ai/auth-client";
import Taro from "@tarojs/taro";

const authClient = createAuthClient();

Page({
  data: {
    userInfo: null,
    hasPhone: false,
  },

  // Auto-login on page load
  onLoad() {
    this.handleLogin();
  },

  // WeChat Mini Program login
  async handleLogin() {
    Taro.showLoading({ title: "Logging in..." });

    const result = await authClient.miniLogin();

    Taro.hideLoading();

    if (result.data) {
      this.setData({
        userInfo: result.data.user,
      });

      // Navigate to home
      Taro.switchTab({ url: "/pages/index/index" });
    } else {
      Taro.showToast({
        title: "Login failed",
        icon: "none",
      });
    }
  },

  // Get phone number with user authorization
  async onGetPhoneNumber(e) {
    const { code } = e.detail;

    if (!code) {
      Taro.showToast({
        title: "Authorization cancelled",
        icon: "none",
      });
      return;
    }

    Taro.showLoading({ title: "Getting phone..." });

    const result = await authClient.miniGetPhone(e);

    Taro.hideLoading();

    if (result.data) {
      this.setData({
        hasPhone: true,
      });

      Taro.showToast({
        title: "Phone number obtained",
        icon: "success",
      });
    } else {
      Taro.showToast({
        title: result.error?.message || "Failed to get phone",
        icon: "none",
      });
    }
  },
});
```

**Low-level compatibility APIs (deprecated, advanced use only):**

If your app already has a login `code` or phone `code`, you can still call:

- `loginWithMiniProgram(code | { code, platform })`
- `getMiniProgramPhoneNumber(code | { code, platform })`

### Logout

```typescript
await authClient.logout();
```

## User Management

### Get Current User

```typescript
const result = await authClient.getMe();
if (result.data) {
  console.log(result.data); // User object
}
```

### Update Profile

```typescript
await authClient.updateMe({
  displayName: "New Name",
  avatarUrl: "https://example.com/avatar.jpg",
});
```

### Change Password

```typescript
await authClient.changePassword({
  oldPassword: "OldPassword@123",
  newPassword: "NewPassword@123",
});
```

## Permission Checks

### Anonymous Access Support

The SDK automatically supports anonymous users with **zero configuration**:

```typescript
// Create client
const authClient = createAuthClient();

// Permission checks work for both authenticated and anonymous users
if (authClient.hasPermission("article", "read")) {
  showArticleList();
}

// Check if user is anonymous
if (authClient.isAnonymous()) {
  showLoginPrompt();
}

// After login, permissions automatically update
await authClient.login({ ... });
```

### Local Permission Checks (Fast)

Permissions are cached locally for fast UI checks:

```typescript
// Check role
if (authClient.hasRole("admin")) {
  showAdminPanel();
}

if (authClient.isAnonymous()) {
  showLoginPrompt();
}

// Check permission
if (authClient.hasPermission("user", "read")) {
  showUserList();
}
```

````

## OAuth Bindings

### Get Bindings

```typescript
const result = await authClient.getOAuthBindings();
if (result.data) {
  console.log(result.data); // Array of OAuthBinding
}
```

### Bind OAuth Account

```typescript
authClient.bindOAuth("google"); // Redirects to OAuth flow
```

### Unbind OAuth Account

```typescript
await authClient.unbindOAuth("google");
```

## Session Management

### Get Sessions

```typescript
const result = await authClient.getSessions();
if (result.data) {
  result.data.forEach((session) => {
    console.log(session.ip, session.userAgent, session.isCurrent);
  });
}
```

### Revoke Session

```typescript
await authClient.revokeSession("session-id");
```

### Revoke All Sessions

```typescript
const result = await authClient.revokeAllSessions();
if (result.data) {
  console.log(`Revoked ${result.data.revokedCount} sessions`);
}
```

## Event System

### Available Events

- `login` - User logged in, callback: `(user: User) => void`
- `logout` - User logged out, callback: `() => void`
- `tokenExpired` - Token expired, callback: `() => void`
- `tokenRefreshed` - Token refreshed, callback: `(token: string) => void`
- `unauthorized` - Unauthorized (401), callback: `() => void`

### Usage

```typescript
// Subscribe to events
authClient.on("unauthorized", () => {
  authClient.clearAuth();
  window.location.href = "/login";
});

authClient.on("login", (user) => {
  console.log("User logged in:", user.displayName);
});

authClient.on("tokenRefreshed", (token) => {
  console.log("Token refreshed");
});

// Unsubscribe
const handler = () => console.log("Logged out");
authClient.on("logout", handler);
authClient.off("logout", handler);
```

## Utility Methods

### Check Authentication Status

```typescript
if (authClient.isAuthenticated()) {
  // User is logged in
}
```

### Get Access Token

```typescript
const token = authClient.getAccessToken();
```

### Set Access Token

```typescript
authClient.setAccessToken("your-token-here");
```

### Clear Auth Data

```typescript
authClient.clearAuth(); // Clears token and user data
```

## Error Handling

```typescript
try {
  await authClient.login({ ... });
} catch (error) {
  // Network error or exception
  console.error("Login failed:", error);
}

// Or use result pattern
const result = await authClient.login({ ... });
if (result.error) {
  // API error response
  if (result.error.status === 401) {
    alert("Invalid email or password");
  } else if (result.error.status === 403) {
    alert("Account is disabled");
  } else {
    alert(`Login failed: ${result.error.message}`);
  }
}
```

## Best Practices

### Token Management

- Tokens are automatically managed by the SDK
- Access Token refreshes 5 minutes before expiration
- No manual token handling required

### Permission Checks

- Use permission checks for UI control (show/hide buttons, menus)
- Frontend checks are NOT security measures
- Backend must always verify permissions

### SSR Support

The SDK works in both browser and SSR environments:

```typescript
const authClient = createAuthClient();
```

## TypeScript

Full TypeScript support:

```typescript
import type {
  User,
  LoginResponse,
  Session,
  OAuthBinding,
} from "@amaster.ai/auth-client";
```

## License

MIT

## Contributing

Contributions are welcome! Please read our contributing guidelines before submitting PRs.
````
