Extract auth module: standalone PocketBase OAuth2 + Graph token management
- Create reusable auth-module/ with frontend and backend components - Frontend: PocketBaseAuth class for OAuth2 login and token management - Backend: GraphTokenManager and PocketBaseValidator for token operations - Includes TypeScript types and comprehensive README - No project-specific dependencies - ready to use in other projects
This commit is contained in:
@@ -0,0 +1,244 @@
|
|||||||
|
# Auth Module
|
||||||
|
|
||||||
|
Standalone authentication module for PocketBase OAuth2 + Microsoft Graph integration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Frontend**: PocketBase OAuth2 authentication with Microsoft provider
|
||||||
|
- **Backend**: Microsoft Graph token management with automatic caching
|
||||||
|
- **No dependencies on project-specific code** - Use in any project
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Copy the `auth-module` folder to your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r auth-module /path/to/your/project/
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add pocketbase @azure/msal-node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Usage
|
||||||
|
|
||||||
|
### Basic Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PocketBaseAuth } from './auth-module/frontend';
|
||||||
|
|
||||||
|
const auth = new PocketBaseAuth({
|
||||||
|
pbUrl: 'http://localhost:8090',
|
||||||
|
collection: 'Users',
|
||||||
|
provider: 'microsoft',
|
||||||
|
loginContainerId: 'loginContainer',
|
||||||
|
userDisplayNameId: 'userDisplayName',
|
||||||
|
userEmailId: 'userEmailValue',
|
||||||
|
loginBtnId: 'loginBtn',
|
||||||
|
loginErrorId: 'loginError',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Required
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Login container (shown when not authenticated) -->
|
||||||
|
<div id="loginContainer" class="hidden">
|
||||||
|
<button id="loginBtn">Login with Microsoft</button>
|
||||||
|
<div id="loginError" class="hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User display (shown when authenticated) -->
|
||||||
|
<div id="userDisplayName"></div>
|
||||||
|
<div id="userEmailValue"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Callbacks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const auth = new PocketBaseAuth(config, {
|
||||||
|
onAuthSuccess: (state) => {
|
||||||
|
console.log('Logged in:', state.user.email);
|
||||||
|
// Initialize other systems here
|
||||||
|
},
|
||||||
|
onAuthFailure: (error) => {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
},
|
||||||
|
onTokenUpdate: (state) => {
|
||||||
|
console.log('Token refreshed');
|
||||||
|
},
|
||||||
|
onUiUpdate: (state) => {
|
||||||
|
console.log('UI updated');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get current auth state
|
||||||
|
const state = auth.getAuthState();
|
||||||
|
console.log(state.isAuthenticated, state.user);
|
||||||
|
|
||||||
|
// Check token status
|
||||||
|
const status = await auth.checkTokenStatus();
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
auth.logout();
|
||||||
|
|
||||||
|
// Get raw PocketBase instance if needed
|
||||||
|
const pb = auth.getPocketBase();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Usage
|
||||||
|
|
||||||
|
### Setup (server.ts)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GraphTokenManager, PocketBaseValidator, BackendAuth } from './auth-module/backend';
|
||||||
|
|
||||||
|
// Option 1: Use individual managers
|
||||||
|
const graphMgr = new GraphTokenManager({
|
||||||
|
clientId: process.env.CLIENT_ID,
|
||||||
|
tenantId: process.env.TENANT_ID,
|
||||||
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pbValidator = new PocketBaseValidator('http://127.0.0.1:8090');
|
||||||
|
|
||||||
|
// Option 2: Use combined manager
|
||||||
|
const backendAuth = new BackendAuth({
|
||||||
|
clientId: process.env.CLIENT_ID,
|
||||||
|
tenantId: process.env.TENANT_ID,
|
||||||
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Graph Token
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get token string
|
||||||
|
const token = await graphMgr.getToken();
|
||||||
|
|
||||||
|
// Get token with expiration
|
||||||
|
const { token, expiresOnISO } = await graphMgr.getTokenWithExpiry();
|
||||||
|
|
||||||
|
// Check if cached and valid
|
||||||
|
if (graphMgr.isTokenValid()) {
|
||||||
|
console.log('Using cached token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache (force refresh)
|
||||||
|
graphMgr.clearCache();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate PocketBase Token
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Validate token
|
||||||
|
const isValid = await pbValidator.validateUserToken(token);
|
||||||
|
|
||||||
|
// Get user record
|
||||||
|
const user = await pbValidator.getUserRecord(token);
|
||||||
|
|
||||||
|
// Get PocketBase instance
|
||||||
|
const pb = pbValidator.getPocketBase();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Endpoint Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app.get('/api/graph/status', async (c) => {
|
||||||
|
try {
|
||||||
|
const { token, expiresOnISO } = await graphMgr.getTokenWithExpiry();
|
||||||
|
return c.json({
|
||||||
|
active: true,
|
||||||
|
expiresOnISO,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{ active: false, error: error.message },
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/submit', async (c) => {
|
||||||
|
const body = await c.req.json();
|
||||||
|
const pbToken = body.pbToken;
|
||||||
|
|
||||||
|
if (!pbToken) {
|
||||||
|
return c.json({ error: 'Missing pbToken' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await pbValidator.validateUserToken(pbToken);
|
||||||
|
if (!isValid) {
|
||||||
|
return c.json({ error: 'Invalid token' }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is valid, proceed with business logic
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required in `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
CLIENT_ID=your-microsoft-app-id
|
||||||
|
TENANT_ID=your-azure-tenant-id
|
||||||
|
CLIENT_SECRET=your-microsoft-client-secret
|
||||||
|
PB_DB=http://127.0.0.1:8090 # PocketBase URL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Frontend Config Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AuthConfig {
|
||||||
|
pbUrl?: string; // PocketBase URL (default: http://localhost:8090)
|
||||||
|
collection?: string; // Auth collection (default: Users)
|
||||||
|
provider?: string; // OAuth provider (default: microsoft)
|
||||||
|
loginContainerId?: string; // ID of login container element
|
||||||
|
userDisplayNameId?: string; // ID of user name display element
|
||||||
|
userEmailId?: string; // ID of user email display element
|
||||||
|
loginBtnId?: string; // ID of login button element
|
||||||
|
loginErrorId?: string; // ID of error message element
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Config Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BackendAuthConfig {
|
||||||
|
clientId?: string; // Microsoft app ID (or CLIENT_ID env var)
|
||||||
|
tenantId?: string; // Azure tenant ID (or TENANT_ID env var)
|
||||||
|
clientSecret?: string; // Client secret (or CLIENT_SECRET env var)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
The module exports TypeScript types for type safety:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AuthState, AuthConfig, GraphTokenCache } from './auth-module/types';
|
||||||
|
|
||||||
|
const state: AuthState = auth.getAuthState();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Frontend** manages user authentication only
|
||||||
|
- **Backend** manages Graph API tokens (never exposed to client)
|
||||||
|
- Tokens are cached in-memory on backend with automatic refresh
|
||||||
|
- Module does not include project-specific features (alerts, etc.)
|
||||||
|
- Each project can implement its own business logic on top
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as parent project
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
import { GraphTokenCache, BackendAuthConfig } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft Graph Token Management (Backend)
|
||||||
|
* Handles token acquisition and caching for backend Graph API calls
|
||||||
|
*/
|
||||||
|
export class GraphTokenManager {
|
||||||
|
private cca: ConfidentialClientApplication;
|
||||||
|
private cache: GraphTokenCache | null = null;
|
||||||
|
private config: Required<BackendAuthConfig>;
|
||||||
|
|
||||||
|
constructor(config: BackendAuthConfig) {
|
||||||
|
this.config = {
|
||||||
|
clientId: config.clientId || process.env.CLIENT_ID || '',
|
||||||
|
tenantId: config.tenantId || process.env.TENANT_ID || '',
|
||||||
|
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cca = new ConfidentialClientApplication({
|
||||||
|
auth: {
|
||||||
|
clientId: this.config.clientId,
|
||||||
|
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
|
||||||
|
clientSecret: this.config.clientSecret,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Graph token (from cache or acquire new)
|
||||||
|
*/
|
||||||
|
async getToken(): Promise<string> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Check cache validity (with 60s buffer for expiration)
|
||||||
|
if (this.cache && this.cache.expiresOn - 60000 > now) {
|
||||||
|
return this.cache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.cca.acquireTokenByClientCredential({
|
||||||
|
scopes: ['https://graph.microsoft.com/.default'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result?.accessToken) {
|
||||||
|
throw new Error('Failed to acquire Graph token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresOn = result.expiresOn
|
||||||
|
? new Date(result.expiresOn).getTime()
|
||||||
|
: now + 55 * 60 * 1000; // Default 55 minutes
|
||||||
|
|
||||||
|
this.cache = {
|
||||||
|
token: result.accessToken,
|
||||||
|
expiresOn,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result.accessToken;
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
throw new Error(`Failed to acquire Graph token: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get token with expiration info
|
||||||
|
*/
|
||||||
|
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
|
||||||
|
const token = await this.getToken();
|
||||||
|
const expiresOn = this.cache?.expiresOn || Date.now();
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
expiresOnISO: new Date(expiresOn).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if token is cached and valid
|
||||||
|
*/
|
||||||
|
isTokenValid(): boolean {
|
||||||
|
if (!this.cache) return false;
|
||||||
|
const now = Date.now();
|
||||||
|
return this.cache.expiresOn - 60000 > now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache (force refresh on next call)
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PocketBase Token Validation (Backend)
|
||||||
|
* Validates and uses user PocketBase tokens
|
||||||
|
*/
|
||||||
|
export class PocketBaseValidator {
|
||||||
|
private pb: PocketBase;
|
||||||
|
|
||||||
|
constructor(pbUrl?: string) {
|
||||||
|
this.pb = new PocketBase(pbUrl || process.env.PB_DB || 'http://127.0.0.1:8090');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user token and validate it
|
||||||
|
*/
|
||||||
|
async validateUserToken(token: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
this.pb.authStore.save(token, null);
|
||||||
|
await this.pb.collection('Users').authRefresh();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token validation failed:', error instanceof Error ? error.message : error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user record from token
|
||||||
|
*/
|
||||||
|
async getUserRecord(token: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
this.pb.authStore.save(token, null);
|
||||||
|
const record = this.pb.authStore.record || this.pb.authStore.model;
|
||||||
|
return record;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get user record:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PocketBase instance
|
||||||
|
*/
|
||||||
|
getPocketBase(): PocketBase {
|
||||||
|
return this.pb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined backend auth manager
|
||||||
|
*/
|
||||||
|
export class BackendAuth {
|
||||||
|
graphTokenManager: GraphTokenManager;
|
||||||
|
pbValidator: PocketBaseValidator;
|
||||||
|
|
||||||
|
constructor(config: BackendAuthConfig, pbUrl?: string) {
|
||||||
|
this.graphTokenManager = new GraphTokenManager(config);
|
||||||
|
this.pbValidator = new PocketBaseValidator(pbUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to validate PocketBase token in requests
|
||||||
|
* Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next))
|
||||||
|
*/
|
||||||
|
async validateTokenMiddleware(c: any, next: any): Promise<any> {
|
||||||
|
const token = c.req.header('Authorization')?.replace('Bearer ', '') ||
|
||||||
|
(await c.req.json().catch(() => ({})))?.pbToken;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const isValid = await this.pbValidator.validateUserToken(token);
|
||||||
|
if (!isValid) {
|
||||||
|
return c.json({ error: 'Invalid token' }, 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
import { AuthConfig, AuthState, AuthCallbacks } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PocketBase OAuth2 Frontend Module
|
||||||
|
* Handles user authentication and token management
|
||||||
|
*/
|
||||||
|
export class PocketBaseAuth {
|
||||||
|
private pb: PocketBase;
|
||||||
|
private config: Required<AuthConfig>;
|
||||||
|
private callbacks: AuthCallbacks;
|
||||||
|
private state: AuthState;
|
||||||
|
|
||||||
|
constructor(config: AuthConfig, callbacks?: Partial<AuthCallbacks>) {
|
||||||
|
this.config = {
|
||||||
|
pbUrl: config.pbUrl || 'http://localhost:8090',
|
||||||
|
collection: config.collection || 'Users',
|
||||||
|
provider: config.provider || 'microsoft',
|
||||||
|
loginContainerId: config.loginContainerId || 'loginContainer',
|
||||||
|
userDisplayNameId: config.userDisplayNameId || 'userDisplayName',
|
||||||
|
userEmailId: config.userEmailId || 'userEmailValue',
|
||||||
|
loginBtnId: config.loginBtnId || 'loginBtn',
|
||||||
|
loginErrorId: config.loginErrorId || 'loginError',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.callbacks = {
|
||||||
|
onAuthSuccess: callbacks?.onAuthSuccess || (() => {}),
|
||||||
|
onAuthFailure: callbacks?.onAuthFailure || (() => {}),
|
||||||
|
onTokenUpdate: callbacks?.onTokenUpdate || (() => {}),
|
||||||
|
onUiUpdate: callbacks?.onUiUpdate || (() => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pb = new PocketBase(this.config.pbUrl);
|
||||||
|
this.state = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize auth module
|
||||||
|
*/
|
||||||
|
private init(): void {
|
||||||
|
this.updateAuthUI();
|
||||||
|
if (this.pb.authStore.isValid && this.pb.authStore.token) {
|
||||||
|
this.ensureUserLogged();
|
||||||
|
}
|
||||||
|
this.setupLoginButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup login button click handler
|
||||||
|
*/
|
||||||
|
private setupLoginButton(): void {
|
||||||
|
const loginBtn = document.getElementById(this.config.loginBtnId);
|
||||||
|
if (!loginBtn) {
|
||||||
|
console.warn(`Login button with id "${this.config.loginBtnId}" not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginBtn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await this.handleLogin();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle login button click
|
||||||
|
*/
|
||||||
|
private async handleLogin(): Promise<void> {
|
||||||
|
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||||
|
const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement;
|
||||||
|
|
||||||
|
if (!loginBtn || !loginError) return;
|
||||||
|
|
||||||
|
loginBtn.disabled = true;
|
||||||
|
loginBtn.textContent = 'Checking session...';
|
||||||
|
loginError.classList.add('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to use existing token first
|
||||||
|
const hadToken = await this.ensureUserLogged();
|
||||||
|
if (hadToken) {
|
||||||
|
this.resetLoginButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise perform OAuth
|
||||||
|
loginBtn.textContent = 'Logging in...';
|
||||||
|
const authData = await this.pb.collection(this.config.collection).authWithOAuth2({
|
||||||
|
provider: this.config.provider,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateState(authData);
|
||||||
|
this.updateAuthUI();
|
||||||
|
this.callbacks.onAuthSuccess?.(this.state);
|
||||||
|
|
||||||
|
console.log('✓ Logged in with', this.config.provider);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error('Login failed:', message);
|
||||||
|
loginError.textContent = `Login failed: ${message}`;
|
||||||
|
loginError.classList.remove('hidden');
|
||||||
|
this.callbacks.onAuthFailure?.(error);
|
||||||
|
} finally {
|
||||||
|
this.resetLoginButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure user is logged in (check existing token)
|
||||||
|
*/
|
||||||
|
async ensureUserLogged(): Promise<boolean> {
|
||||||
|
if (!this.pb.authStore.isValid || !this.pb.authStore.token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refresh = await this.pb.collection(this.config.collection).authRefresh();
|
||||||
|
this.updateState(refresh);
|
||||||
|
this.updateAuthUI();
|
||||||
|
this.callbacks.onTokenUpdate?.(this.state);
|
||||||
|
console.log('✓ Token refreshed');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Existing token invalid, clearing auth');
|
||||||
|
this.pb.authStore.clear();
|
||||||
|
this.updateState(null);
|
||||||
|
this.updateAuthUI();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update internal auth state
|
||||||
|
*/
|
||||||
|
private updateState(data: any): void {
|
||||||
|
if (!data) {
|
||||||
|
this.state = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = data.record || this.pb.authStore.record || this.pb.authStore.model;
|
||||||
|
const meta = data.meta || {};
|
||||||
|
const model = this.pb.authStore.model;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAuthenticated: true,
|
||||||
|
user: {
|
||||||
|
id: record?.id || model?.id || 'unknown',
|
||||||
|
name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User',
|
||||||
|
email: record?.email || model?.email || meta?.email || '(no email)',
|
||||||
|
},
|
||||||
|
token: this.pb.authStore.token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI based on auth state
|
||||||
|
*/
|
||||||
|
updateAuthUI(): void {
|
||||||
|
const loginContainer = document.getElementById(this.config.loginContainerId);
|
||||||
|
const userDisplayName = document.getElementById(this.config.userDisplayNameId);
|
||||||
|
const userEmail = document.getElementById(this.config.userEmailId);
|
||||||
|
|
||||||
|
if (!loginContainer) return;
|
||||||
|
|
||||||
|
if (this.pb.authStore.isValid) {
|
||||||
|
loginContainer.classList.add('hidden');
|
||||||
|
if (this.state.user) {
|
||||||
|
if (userDisplayName) userDisplayName.textContent = this.state.user.name;
|
||||||
|
if (userEmail) userEmail.textContent = this.state.user.email;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loginContainer.classList.remove('hidden');
|
||||||
|
const loginError = document.getElementById(this.config.loginErrorId);
|
||||||
|
if (loginError) loginError.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks.onUiUpdate?.(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check token status (for verification/display)
|
||||||
|
*/
|
||||||
|
async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> {
|
||||||
|
const pbToken = !!this.pb.authStore.token;
|
||||||
|
return { pbToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current auth state
|
||||||
|
*/
|
||||||
|
getAuthState(): AuthState {
|
||||||
|
return { ...this.state };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PocketBase instance (for direct usage if needed)
|
||||||
|
*/
|
||||||
|
getPocketBase(): PocketBase {
|
||||||
|
return this.pb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout
|
||||||
|
*/
|
||||||
|
logout(): void {
|
||||||
|
this.pb.authStore.clear();
|
||||||
|
this.updateState(null);
|
||||||
|
this.updateAuthUI();
|
||||||
|
console.log('✓ Logged out');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset login button to initial state
|
||||||
|
*/
|
||||||
|
private resetLoginButton(): void {
|
||||||
|
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||||
|
if (loginBtn) {
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.textContent = 'Login with Microsoft';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick init function for simple use cases
|
||||||
|
*/
|
||||||
|
export async function initPocketBaseAuth(config: AuthConfig): Promise<PocketBaseAuth> {
|
||||||
|
const auth = new PocketBaseAuth(config);
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Frontend Auth Configuration
|
||||||
|
*/
|
||||||
|
export interface AuthConfig {
|
||||||
|
pbUrl?: string;
|
||||||
|
collection?: string;
|
||||||
|
provider?: string;
|
||||||
|
loginContainerId?: string;
|
||||||
|
userDisplayNameId?: string;
|
||||||
|
userEmailId?: string;
|
||||||
|
loginBtnId?: string;
|
||||||
|
loginErrorId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend Auth Configuration
|
||||||
|
*/
|
||||||
|
export interface BackendAuthConfig {
|
||||||
|
clientId?: string;
|
||||||
|
tenantId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth state object
|
||||||
|
*/
|
||||||
|
export interface AuthState {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graph token cache object
|
||||||
|
*/
|
||||||
|
export interface GraphTokenCache {
|
||||||
|
token: string;
|
||||||
|
expiresOn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth event callbacks
|
||||||
|
*/
|
||||||
|
export interface AuthCallbacks {
|
||||||
|
onAuthSuccess?: (state: AuthState) => void;
|
||||||
|
onAuthFailure?: (error: any) => void;
|
||||||
|
onTokenUpdate?: (state: AuthState) => void;
|
||||||
|
onUiUpdate?: (state: AuthState) => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user