Add Mode1Svelte - independent copy of Mode1 for future SvelteKit integration
- Created Mode1Svelte as complete standalone project - Identical to Mode1 currently (Hono backend + Vite frontend) - Separate auth-module copy (no dependencies on Mode1) - Updated orchestrator to default to Mode1Svelte - Both Mode1 and Mode1Svelte are independent and interchangeable via ModeSwitch - Ready for gradual Svelte/SvelteKit migration while maintaining API compatibility
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# Mode1Svelte - Auth Module Test Suite
|
||||
|
||||
Self-contained test environment for the auth-module. This version is identical to Mode1 currently, serving as a foundation for future Svelte integration.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Mode1Svelte/
|
||||
├── auth-module/ # Local copy of auth-module (not linked to root)
|
||||
├── backend/ # Backend server with auth endpoints
|
||||
│ └── server.ts # Hono app with Graph token and PB validation APIs
|
||||
├── frontend/ # Frontend test interface
|
||||
│ └── index.html # Interactive test dashboard
|
||||
├── logs/ # Application logs
|
||||
├── package.json # Dependencies (auth module + Hono + PocketBase)
|
||||
└── bun.lock # Locked dependencies
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
✓ **Frontend Auth (PocketBaseAuth)**
|
||||
- OAuth2 login initialization
|
||||
- Auth state retrieval
|
||||
- Token refresh
|
||||
- Logout
|
||||
|
||||
✓ **Backend Auth (GraphTokenManager)**
|
||||
- Microsoft Graph token acquisition
|
||||
- Token caching (with 60s expiration buffer)
|
||||
- Cache validity checking
|
||||
- Cache refresh
|
||||
|
||||
✓ **PocketBase Validation (PocketBaseValidator)**
|
||||
- User token validation
|
||||
- User record retrieval
|
||||
- Token comparison (Graph vs PocketBase)
|
||||
|
||||
## Running Mode1Svelte
|
||||
|
||||
### Via Orchestrator (recommended)
|
||||
|
||||
```bash
|
||||
# Switch to Mode1Svelte
|
||||
curl -X POST http://localhost:3006/switch/Mode1Svelte
|
||||
|
||||
# Check status
|
||||
curl http://localhost:3006/status
|
||||
```
|
||||
|
||||
### Direct
|
||||
|
||||
```bash
|
||||
cd Mode1Svelte
|
||||
bun install
|
||||
PORT=3005 bun run backend/server.ts
|
||||
```
|
||||
|
||||
Then visit: `http://localhost:3005`
|
||||
|
||||
## Future: Svelte Integration
|
||||
|
||||
This project is set up as an independent Mode to eventually convert to SvelteKit while maintaining the same API endpoints and auth-module integration.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Mode1/
|
||||
├── auth-module/ # Local copy of auth-module (not linked to root)
|
||||
├── backend/ # Backend server with auth endpoints
|
||||
│ └── server.ts # Hono app with Graph token and PB validation APIs
|
||||
├── frontend/ # Frontend test interface
|
||||
│ └── index.html # Interactive test dashboard
|
||||
├── logs/ # Application logs
|
||||
├── package.json # Dependencies (auth module + Hono + PocketBase)
|
||||
└── bun.lock # Locked dependencies
|
||||
```
|
||||
|
||||
## Features Tested
|
||||
|
||||
✓ **Frontend Auth (PocketBaseAuth)**
|
||||
- OAuth2 login initialization
|
||||
- Auth state retrieval
|
||||
- Token refresh
|
||||
- Logout
|
||||
|
||||
✓ **Backend Auth (GraphTokenManager)**
|
||||
- Microsoft Graph token acquisition
|
||||
- Token caching (with 60s expiration buffer)
|
||||
- Cache validity checking
|
||||
- Cache refresh
|
||||
|
||||
✓ **PocketBase Validation (PocketBaseValidator)**
|
||||
- User token validation
|
||||
- User record retrieval
|
||||
|
||||
## Running Mode1
|
||||
|
||||
### Via Orchestrator (recommended)
|
||||
|
||||
```bash
|
||||
# Switch to Mode1
|
||||
curl -X POST http://localhost:3006/switch/Mode1
|
||||
|
||||
# Check status
|
||||
curl http://localhost:3006/status
|
||||
```
|
||||
|
||||
### Direct
|
||||
|
||||
```bash
|
||||
# Install dependencies (already done)
|
||||
bun install
|
||||
|
||||
# Start server
|
||||
bun run backend/server.ts
|
||||
|
||||
# Access dashboard
|
||||
open http://localhost:3005
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Configuration
|
||||
- `GET /api/auth/config` - Get frontend configuration
|
||||
|
||||
### Graph Token Management
|
||||
- `GET /api/auth/graph/token` - Get current Graph token (with expiration)
|
||||
- `POST /api/auth/refresh-graph` - Force refresh Graph token
|
||||
|
||||
### PocketBase Validation
|
||||
- `POST /api/auth/validate-pb-token` - Validate a PocketBase token
|
||||
|
||||
### Health
|
||||
- `GET /health` - Health check
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Uses `/home/admin/secrets/.env` via auth-module:
|
||||
- `CLIENT_ID` - Microsoft application ID
|
||||
- `TENANT_ID` - Azure tenant ID
|
||||
- `CLIENT_SECRET` - Microsoft client secret
|
||||
- `PB_URL` - PocketBase frontend URL
|
||||
- `PB_DB` - PocketBase backend URL
|
||||
|
||||
## Dependencies
|
||||
|
||||
All dependencies are installed locally in `node_modules/`:
|
||||
- `hono` - Web framework
|
||||
- `pocketbase` - PocketBase client
|
||||
- `@azure/msal-node` - Microsoft authentication
|
||||
- `dotenv` - Environment variable loading
|
||||
|
||||
## Notes
|
||||
|
||||
- This Mode1 is **completely self-contained** with its own auth-module copy
|
||||
- No dependencies on root auth-module
|
||||
- Safe to modify and test independently
|
||||
- All secrets come from `/home/admin/secrets/.env`
|
||||
@@ -0,0 +1,248 @@
|
||||
# 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
|
||||
|
||||
**Important**: The frontend is browser-side code. It must receive `pbUrl` from the consuming application (not from the backend).
|
||||
|
||||
```typescript
|
||||
import { PocketBaseAuth } from './auth-module/frontend';
|
||||
|
||||
// In your project, load PB_URL from environment/config and pass it to frontend
|
||||
const auth = new PocketBaseAuth({
|
||||
pbUrl: process.env.PB_URL, // Required - must be provided by consuming application
|
||||
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(process.env.PB_DB);
|
||||
|
||||
// Option 2: Use combined manager
|
||||
const backendAuth = new BackendAuth({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
}, process.env.PB_DB);
|
||||
```
|
||||
|
||||
### 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=https://your-pocketbase-instance.com
|
||||
PB_URL=https://your-pocketbase-instance.com
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Frontend Config Options
|
||||
|
||||
```typescript
|
||||
interface AuthConfig {
|
||||
pbUrl: string; // PocketBase URL (required)
|
||||
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,204 @@
|
||||
import { config } from 'dotenv';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { GraphTokenCache, BackendAuthConfig } from './types';
|
||||
|
||||
// Load environment variables from shared secrets directory
|
||||
config({ path: '/home/admin/secrets/.env' });
|
||||
|
||||
/**
|
||||
* Configuration Manager
|
||||
* Exposes frontend-safe configuration loaded from environment
|
||||
*/
|
||||
export class AuthConfigManager {
|
||||
/**
|
||||
* Get frontend configuration (safe to expose to browser)
|
||||
*/
|
||||
static getFrontendConfig() {
|
||||
return {
|
||||
pbUrl: process.env.PB_URL!,
|
||||
provider: 'microsoft',
|
||||
collection: 'Users',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all backend secrets (never expose to frontend)
|
||||
*/
|
||||
static getBackendConfig() {
|
||||
return {
|
||||
clientId: process.env.CLIENT_ID!,
|
||||
tenantId: process.env.TENANT_ID!,
|
||||
clientSecret: process.env.CLIENT_SECRET!,
|
||||
pbDb: process.env.PB_DB!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,250 @@
|
||||
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!,
|
||||
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 - fetches config from backend
|
||||
*/
|
||||
export async function initPocketBaseAuth(
|
||||
backendUrl: string,
|
||||
callbacks?: Partial<AuthCallbacks>
|
||||
): Promise<PocketBaseAuth> {
|
||||
// Fetch frontend config from backend
|
||||
const response = await fetch(`${backendUrl}/api/auth/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch auth configuration from backend');
|
||||
}
|
||||
const config = await response.json();
|
||||
|
||||
const auth = new PocketBaseAuth(config, callbacks);
|
||||
return auth;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Frontend Auth Configuration
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
pbUrl: string; // PocketBase URL (required)
|
||||
collection?: string;
|
||||
provider?: string;
|
||||
loginContainerId?: string;
|
||||
userDisplayNameId?: string;
|
||||
userEmailId?: string;
|
||||
loginBtnId?: string;
|
||||
loginErrorId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend configuration provided by backend
|
||||
*/
|
||||
export interface FrontendConfig {
|
||||
pbUrl: string;
|
||||
provider?: string;
|
||||
collection?: 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;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { cors } from 'hono/cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { AuthConfigManager, BackendAuth } from '../auth-module/backend';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const app = new Hono();
|
||||
const PORT = process.env.PORT || 3005;
|
||||
|
||||
// Initialize auth
|
||||
const backendAuth = new BackendAuth({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
}, process.env.PB_DB);
|
||||
|
||||
// CORS Configuration - Allow requests from frontend domains
|
||||
app.use('*', cors({
|
||||
origin: ['https://ji-test.ccllc.pro', 'http://localhost:3005', 'http://localhost:5173'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Serve bundled auth module JavaScript
|
||||
app.get('/auth-module.js', async (c) => {
|
||||
const filePath = path.join(__dirname, '../frontend/dist/auth.js');
|
||||
if (existsSync(filePath)) {
|
||||
const content = await readFile(filePath);
|
||||
c.header('Content-Type', 'application/javascript');
|
||||
return c.body(content);
|
||||
}
|
||||
return c.notFound();
|
||||
});
|
||||
|
||||
// Serve frontend static files
|
||||
app.use('/', serveStatic({ root: path.join(__dirname, '../frontend') }));
|
||||
|
||||
// API: Get frontend config
|
||||
app.get('/api/auth/config', (c) => {
|
||||
try {
|
||||
const config = AuthConfigManager.getFrontendConfig();
|
||||
return c.json(config);
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ error: 'Failed to get config', message: error instanceof Error ? error.message : 'Unknown error' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Get Graph token status
|
||||
app.get('/api/auth/graph/token', async (c) => {
|
||||
try {
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!token) {
|
||||
return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500);
|
||||
}
|
||||
|
||||
// Decode JWT to show token details (for verification)
|
||||
const parts = token.split('.');
|
||||
let payload = {};
|
||||
if (parts.length === 3) {
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
payload = {
|
||||
aud: decoded.aud,
|
||||
iss: decoded.iss,
|
||||
scp: decoded.scp,
|
||||
app_displayname: decoded.app_displayname,
|
||||
iat: new Date(decoded.iat * 1000).toISOString(),
|
||||
exp: new Date(decoded.exp * 1000).toISOString(),
|
||||
};
|
||||
} catch (e) {
|
||||
// Silent fail - just show truncated token
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
token: token.substring(0, 50) + '...',
|
||||
fullToken: token, // Include full token for testing
|
||||
expiresOn: expiresOnISO,
|
||||
isValid: backendAuth.graphTokenManager.isTokenValid(),
|
||||
tokenDetails: Object.keys(payload).length > 0 ? payload : 'Could not decode',
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to acquire token' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Validate PocketBase token
|
||||
app.post('/api/auth/validate-pb-token', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const token = body.pbToken;
|
||||
|
||||
if (!token) {
|
||||
return c.json({ valid: false, error: 'No token provided' }, 400);
|
||||
}
|
||||
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(token);
|
||||
const user = isValid ? await backendAuth.pbValidator.getUserRecord(token) : null;
|
||||
|
||||
return c.json({
|
||||
valid: isValid,
|
||||
user: user ? {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
} : null,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ valid: false, error: error instanceof Error ? error.message : 'Validation failed' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Refresh Graph token (test cache)
|
||||
app.post('/api/auth/refresh-graph', async (c) => {
|
||||
try {
|
||||
backendAuth.graphTokenManager.clearCache();
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
return c.json({
|
||||
success: true,
|
||||
refreshed: true,
|
||||
expiresOn: expiresOnISO,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Refresh failed' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Compare PB and Graph tokens
|
||||
app.post('/api/auth/compare-tokens', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const pbToken = body.pbToken;
|
||||
|
||||
// Get Graph token
|
||||
const { token: graphToken, expiresOnISO: graphExpires } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!graphToken) {
|
||||
return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500);
|
||||
}
|
||||
|
||||
// Decode both tokens to compare
|
||||
const decodeJWT = (token: string) => {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
return JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const graphPayload = decodeJWT(graphToken);
|
||||
|
||||
// Validate PB token to get actual user
|
||||
let authenticatedUser = 'Unknown';
|
||||
if (pbToken) {
|
||||
try {
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(pbToken);
|
||||
if (isValid) {
|
||||
const userRecord = await backendAuth.pbValidator.getUserRecord(pbToken);
|
||||
authenticatedUser = userRecord?.email || userRecord?.id || 'Unknown';
|
||||
}
|
||||
} catch {
|
||||
authenticatedUser = 'Token invalid or expired';
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
comparison: {
|
||||
graph_token: {
|
||||
issuer: graphPayload?.iss || 'N/A',
|
||||
audience: graphPayload?.aud || 'N/A',
|
||||
app: graphPayload?.app_displayname || 'N/A',
|
||||
scopes: graphPayload?.scp || 'N/A',
|
||||
issued_at: graphPayload?.iat ? new Date(graphPayload.iat * 1000).toISOString() : 'N/A',
|
||||
expires_at: graphPayload?.exp ? new Date(graphPayload.exp * 1000).toISOString() : 'N/A',
|
||||
type: 'Microsoft Graph Token (Backend/App-only)',
|
||||
source: '@azure/msal-node',
|
||||
purpose: 'Backend API calls to Microsoft Graph',
|
||||
},
|
||||
pocketbase_token: {
|
||||
note: 'User token from PocketBase OAuth2',
|
||||
type: 'PocketBase User Token (Frontend/User)',
|
||||
source: 'PocketBase OAuth2 flow',
|
||||
purpose: 'User authentication and authorization',
|
||||
authenticated_user: authenticatedUser,
|
||||
},
|
||||
are_different: true,
|
||||
explanation: 'These are completely different tokens from different systems: Graph token is for app-to-app Microsoft API access, PocketBase token is for user authentication in the application.',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to compare tokens' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
console.log(`Mode1 Auth Test Server running on port ${PORT}`);
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mode1-auth-test",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^5.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@azure/msal-common": ["@azure/msal-common@16.0.2", "", {}, "sha512-ZJ/UR7lyqIntURrIJCyvScwJFanM9QhJYcJCheB21jZofGKpP9QxWgvADANo7UkresHKzV+6YwoeZYP7P7HvUg=="],
|
||||
|
||||
"@azure/msal-node": ["@azure/msal-node@5.0.2", "", { "dependencies": { "@azure/msal-common": "16.0.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-3tHeJghckgpTX98TowJoXOjKGuds0L+FKfeHJtoZFl2xvwE6RF65shZJzMQ5EQZWXzh3sE1i9gE+m3aRMachjA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27rypIapNkYboOSylkf1tD9UW9Ado2I+P1NBL46Qz29KmOjTL6WuJ7mHDC5O66CYxlOkF5r93NPDAC3lFHYBXw=="],
|
||||
|
||||
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-I82xGzPkBxzBKgbl8DsA0RfMQCWTWjNmLjIEkW1ECiv3qK02kHGQ5FGUr/29L/SuvnGsULW4tBTRNZiMzL37nA=="],
|
||||
|
||||
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqtr+pTsHqusYpG2OZc6s+AmpWDB/FmBvstrK0y5zkti4OqnCuu7Ev2xNjS7uyb47NrAFF40pWqkpaio5XEd7w=="],
|
||||
|
||||
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-YaQEAYjBanoOOtpqk/c5GGcfZIyxIIkQ2m1TbHjedRmJNwxzWBhGinSARFkrRIc3F8pRIGAopXKvJ/2rjN1LzQ=="],
|
||||
|
||||
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FR+iJt17rfFgYgpxL3M67AUwujOgjw52ZJzB9vElI5jQXNjTyOKf8eH4meSk4vjlYF3h/AjKYd6pmN0OIUlVKQ=="],
|
||||
|
||||
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-egfngj0dfJ868cf30E7B+ye9KUWSebYxOG4l9YP5eWeMXCtenpenx0zdKtAn9qxJgEJym5AN6trtlk+J6x8Lig=="],
|
||||
|
||||
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-jRmnX18ak8WzqLrex3siw0PoVKyIeI5AiCv4wJLgSs7VKfOqrPycfHIWfIX2jdn7ngqbHFPzI09VBKANZ4Pckg=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-YeXcJ9K6vJAt1zSkeA21J6pTe7PgDMLTHKGI3nQBiMYnYf7Ob3K+b/ChSCznrJG7No5PCPiQPg4zTgA+BOTmSA=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-7FjVnxnRTp/AgWqSQRT/Vt9TYmvnZ+4M+d9QOKh/Lf++wIFXFGSeAgD6bV1X/yr2UPVmZDk+xdhr2XkU7l2v3w=="],
|
||||
|
||||
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-Sr1KwUcbB0SEpnSPO22tNJppku2khjFluEst+mTGhxHzAGQTQncNeJxDnt3F15n+p9Q+mlcorxehd68n1siikQ=="],
|
||||
|
||||
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-PFUa7JL4lGoyyppeS4zqfuoXXih+gSE0XxhDMrCPVEUev0yhGNd/tbWBvcdpYnUth80owENoGjc8s5Knopv9wA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.3", "", { "os": "android", "cpu": "arm" }, "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.3", "", { "os": "android", "cpu": "arm64" }, "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.3", "", { "os": "linux", "cpu": "arm" }, "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.3", "", { "os": "linux", "cpu": "arm" }, "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.3", "", { "os": "linux", "cpu": "x64" }, "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.3", "", { "os": "none", "cpu": "arm64" }, "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.3", "", { "os": "win32", "cpu": "x64" }, "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.3", "", { "os": "win32", "cpu": "x64" }, "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bun": ["bun@1.3.6", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.6", "@oven/bun-darwin-x64": "1.3.6", "@oven/bun-darwin-x64-baseline": "1.3.6", "@oven/bun-linux-aarch64": "1.3.6", "@oven/bun-linux-aarch64-musl": "1.3.6", "@oven/bun-linux-x64": "1.3.6", "@oven/bun-linux-x64-baseline": "1.3.6", "@oven/bun-linux-x64-musl": "1.3.6", "@oven/bun-linux-x64-musl-baseline": "1.3.6", "@oven/bun-windows-x64": "1.3.6", "@oven/bun-windows-x64-baseline": "1.3.6" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-Tn98GlZVN2WM7+lg/uGn5DzUao37Yc0PUz7yzYHdeF5hd+SmHQGbCUIKE4Sspdgtxn49LunK3mDNBC2Qn6GJjw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.6", "", {}, "sha512-Pl7V4y3DWglYITC4cBpclmuIzePRGsb/sXk/Wyqxznwu5JsHA5IILJY81PT2XQ3OSKCakWjbxjYBqtdcghzKvA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"rollup": ["rollup@4.55.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.3", "@rollup/rollup-android-arm64": "4.55.3", "@rollup/rollup-darwin-arm64": "4.55.3", "@rollup/rollup-darwin-x64": "4.55.3", "@rollup/rollup-freebsd-arm64": "4.55.3", "@rollup/rollup-freebsd-x64": "4.55.3", "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", "@rollup/rollup-linux-arm-musleabihf": "4.55.3", "@rollup/rollup-linux-arm64-gnu": "4.55.3", "@rollup/rollup-linux-arm64-musl": "4.55.3", "@rollup/rollup-linux-loong64-gnu": "4.55.3", "@rollup/rollup-linux-loong64-musl": "4.55.3", "@rollup/rollup-linux-ppc64-gnu": "4.55.3", "@rollup/rollup-linux-ppc64-musl": "4.55.3", "@rollup/rollup-linux-riscv64-gnu": "4.55.3", "@rollup/rollup-linux-riscv64-musl": "4.55.3", "@rollup/rollup-linux-s390x-gnu": "4.55.3", "@rollup/rollup-linux-x64-gnu": "4.55.3", "@rollup/rollup-linux-x64-musl": "4.55.3", "@rollup/rollup-openbsd-x64": "4.55.3", "@rollup/rollup-openharmony-arm64": "4.55.3", "@rollup/rollup-win32-arm64-msvc": "4.55.3", "@rollup/rollup-win32-ia32-msvc": "4.55.3", "@rollup/rollup-win32-x64-gnu": "4.55.3", "@rollup/rollup-win32-x64-msvc": "4.55.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Frontend entry point - exports auth module for browser
|
||||
export { PocketBaseAuth, initPocketBaseAuth } from '../auth-module/frontend';
|
||||
export type { AuthConfig, AuthState, AuthCallbacks } from '../auth-module/types';
|
||||
@@ -0,0 +1,545 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mode1 - Auth Module Test</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 2rem;
|
||||
color: #f1f5f9;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-icon.success {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.status-icon.pending {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.status-icon.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: background 0.2s;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #64748b;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.output {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.output.success {
|
||||
border-color: #22c55e;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.output.error {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.output.warning {
|
||||
border-color: #f59e0b;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: #1e293b;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Auth Module Test Suite (Mode1)</h1>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Frontend Auth Section -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="status-icon pending" id="frontendStatus"></span>
|
||||
Frontend: PocketBase Auth
|
||||
</h2>
|
||||
|
||||
<div class="info">
|
||||
Tests OAuth2 login, token refresh, and user state management.
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="testFrontendInit()">Initialize Auth</button>
|
||||
<button onclick="testGetAuthState()">Get Auth State</button>
|
||||
</div>
|
||||
<button onclick="testFrontendLogout()">Logout</button>
|
||||
|
||||
<div class="output" id="frontendOutput"></div>
|
||||
<div class="timestamp" id="frontendTime"></div>
|
||||
</div>
|
||||
|
||||
<!-- Backend Auth Section -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="status-icon pending" id="backendStatus"></span>
|
||||
Backend: Graph Token & PB Validation
|
||||
</h2>
|
||||
|
||||
<div class="info">
|
||||
Tests Microsoft Graph token acquisition, caching, and PocketBase token validation.
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="testGraphToken()">Get Graph Token</button>
|
||||
<button onclick="testGraphRefresh()">Refresh Graph Token</button>
|
||||
</div>
|
||||
<button onclick="testCompareTokens()">Compare PB vs Graph</button>
|
||||
<button onclick="testPBValidation()">Validate PB Token</button>
|
||||
<button onclick="testCompareTokens()">Compare PB vs Graph</button>
|
||||
<div class="output" id="backendOutput"></div>
|
||||
<div class="timestamp" id="backendTime"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Info Section -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="status-icon pending" id="jobInfoStatus"></span>
|
||||
Job Info (First 4 Records)
|
||||
</h2>
|
||||
|
||||
<div class="info">
|
||||
Fetch and display first 4 records from Job_Info_Prod collection.
|
||||
</div>
|
||||
|
||||
<button onclick="testJobInfo()">Fetch Job Info</button>
|
||||
|
||||
<div class="output" id="jobInfoOutput"></div>
|
||||
<div class="timestamp" id="jobInfoTime"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<script src="/auth-module.js"></script>
|
||||
<script>
|
||||
let pbAuth = null;
|
||||
let config = null;
|
||||
|
||||
// Utility: Log to output
|
||||
function log(elementId, message, type = 'info') {
|
||||
const output = document.getElementById(elementId);
|
||||
if (!output) return;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const line = `[${timestamp}] ${message}`;
|
||||
|
||||
output.textContent += (output.textContent ? '\n' : '') + line;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
output.className = `output ${type}`;
|
||||
|
||||
// Update timestamp
|
||||
const timeEl = document.getElementById(elementId.replace('Output', 'Time'));
|
||||
if (timeEl) timeEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
function setStatus(elementId, status) {
|
||||
const icon = document.getElementById(elementId);
|
||||
if (!icon) return;
|
||||
icon.className = `status-icon ${status}`;
|
||||
}
|
||||
|
||||
// Test: Load config
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('./api/auth/config');
|
||||
if (!response.ok) throw new Error('Failed to load config');
|
||||
config = await response.json();
|
||||
document.getElementById('configOutput').textContent = JSON.stringify(config, null, 2);
|
||||
document.getElementById('configOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
log('configOutput', `Error loading config: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Initialize Frontend Auth
|
||||
async function testFrontendInit() {
|
||||
setStatus('frontendStatus', 'pending');
|
||||
log('frontendOutput', 'Initializing PocketBaseAuth...');
|
||||
|
||||
try {
|
||||
if (typeof window.AuthModule === 'undefined' || !window.AuthModule.initPocketBaseAuth) {
|
||||
throw new Error('Auth module not loaded');
|
||||
}
|
||||
|
||||
pbAuth = await window.AuthModule.initPocketBaseAuth('.', {
|
||||
onAuthSuccess: (state) => {
|
||||
log('frontendOutput', `✓ Auth success: ${state.user?.email}`, 'success');
|
||||
setStatus('frontendStatus', 'success');
|
||||
},
|
||||
onAuthFailure: (error) => {
|
||||
log('frontendOutput', `✗ Auth failed: ${error.message}`, 'error');
|
||||
setStatus('frontendStatus', 'error');
|
||||
},
|
||||
onTokenUpdate: (state) => {
|
||||
log('frontendOutput', `✓ Token refreshed for ${state.user?.email}`, 'success');
|
||||
},
|
||||
onUiUpdate: (state) => {
|
||||
log('frontendOutput', `✓ UI updated: ${state.isAuthenticated ? 'Authenticated' : 'Not authenticated'}`);
|
||||
},
|
||||
});
|
||||
|
||||
log('frontendOutput', '✓ Frontend auth initialized', 'success');
|
||||
setStatus('frontendStatus', 'success');
|
||||
} catch (error) {
|
||||
log('frontendOutput', `✗ Initialization failed: ${error.message}`, 'error');
|
||||
setStatus('frontendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Get Auth State
|
||||
function testGetAuthState() {
|
||||
if (!pbAuth) {
|
||||
log('frontendOutput', '✗ Auth not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const state = pbAuth.getAuthState();
|
||||
log('frontendOutput', `Auth State: ${JSON.stringify(state, null, 2)}`, 'success');
|
||||
}
|
||||
|
||||
// Test: Frontend Logout
|
||||
function testFrontendLogout() {
|
||||
if (!pbAuth) {
|
||||
log('frontendOutput', '✗ Auth not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
pbAuth.logout();
|
||||
log('frontendOutput', '✓ Logged out', 'success');
|
||||
setStatus('frontendStatus', 'pending');
|
||||
}
|
||||
|
||||
// Test: Get Graph Token
|
||||
async function testGraphToken() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Requesting Graph token from backend...');
|
||||
|
||||
try {
|
||||
const response = await fetch('./api/auth/graph/token');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
let output = `✓ Token acquired\n`;
|
||||
output += `Expires: ${data.expiresOn}\n`;
|
||||
output += `Valid in cache: ${data.isValid}\n\n`;
|
||||
|
||||
if (data.tokenDetails && typeof data.tokenDetails === 'object') {
|
||||
output += `Token Details:\n`;
|
||||
output += ` Audience: ${data.tokenDetails.aud}\n`;
|
||||
output += ` Issuer: ${data.tokenDetails.iss}\n`;
|
||||
output += ` Scopes: ${data.tokenDetails.scp}\n`;
|
||||
output += ` App: ${data.tokenDetails.app_displayname}\n`;
|
||||
output += ` Issued: ${data.tokenDetails.iat}\n`;
|
||||
output += ` Expires: ${data.tokenDetails.exp}\n\n`;
|
||||
}
|
||||
|
||||
output += `Full Token (first 100 chars):\n${data.fullToken.substring(0, 100)}...`;
|
||||
|
||||
log('backendOutput', output, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Request failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Refresh Graph Token
|
||||
async function testGraphRefresh() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Refreshing Graph token...');
|
||||
|
||||
try {
|
||||
const response = await fetch('./api/auth/refresh-graph', { method: 'POST' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
log('backendOutput', `✓ Token refreshed\nExpires: ${data.expiresOn}`, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Refresh failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Compare tokens
|
||||
async function testCompareTokens() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Comparing PocketBase token vs Graph token...');
|
||||
|
||||
if (!pbAuth) {
|
||||
log('backendOutput', '✗ Auth not initialized', 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = pbAuth.getAuthState();
|
||||
const pbToken = state.token || null;
|
||||
|
||||
const response = await fetch('./api/auth/compare-tokens', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.comparison) {
|
||||
let output = `✓ Token Comparison:\n\n`;
|
||||
|
||||
output += `GRAPH TOKEN (Backend/App-only):\n`;
|
||||
output += ` Type: ${data.comparison.graph_token.type}\n`;
|
||||
output += ` Source: ${data.comparison.graph_token.source}\n`;
|
||||
output += ` Issuer: ${data.comparison.graph_token.issuer}\n`;
|
||||
output += ` Audience: ${data.comparison.graph_token.audience}\n`;
|
||||
output += ` App: ${data.comparison.graph_token.app}\n`;
|
||||
output += ` Scopes: ${data.comparison.graph_token.scopes}\n`;
|
||||
output += ` Purpose: ${data.comparison.graph_token.purpose}\n\n`;
|
||||
|
||||
output += `POCKETBASE TOKEN (Frontend/User):\n`;
|
||||
output += ` Type: ${data.comparison.pocketbase_token.type}\n`;
|
||||
output += ` Source: ${data.comparison.pocketbase_token.source}\n`;
|
||||
output += ` User: ${data.comparison.pocketbase_token.authenticated_user}\n`;
|
||||
output += ` Purpose: ${data.comparison.pocketbase_token.purpose}\n\n`;
|
||||
|
||||
output += `COMPARISON:\n`;
|
||||
output += ` Are Different: YES ✓\n`;
|
||||
output += ` ${data.comparison.explanation}\n`;
|
||||
|
||||
log('backendOutput', output, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Comparison failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Validate PocketBase Token
|
||||
async function testPBValidation() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Validating PocketBase token...');
|
||||
|
||||
if (!pbAuth) {
|
||||
log('backendOutput', '✗ Auth not initialized', 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = pbAuth.getAuthState();
|
||||
|
||||
if (!state.token) {
|
||||
log('backendOutput', '✗ No token available - user not logged in', 'warning');
|
||||
setStatus('backendStatus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
log('backendOutput', `Sending token for user: ${state.user?.email}...`);
|
||||
|
||||
const response = await fetch('./api/auth/validate-pb-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken: state.token }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.valid) {
|
||||
log('backendOutput', `✓ Token valid\nUser: ${data.user?.email}\nID: ${data.user?.id}`, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ Token invalid: ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Validation failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Fetch Job Info
|
||||
async function testJobInfo() {
|
||||
setStatus('jobInfoStatus', 'pending');
|
||||
log('jobInfoOutput', 'Fetching first 4 records from Job_Info_Prod...');
|
||||
|
||||
if (!pbAuth) {
|
||||
log('jobInfoOutput', '✗ Auth not initialized - cannot fetch records', 'error');
|
||||
setStatus('jobInfoStatus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pb = pbAuth.getPocketBase();
|
||||
const records = await pb.collection('Job_Info_Prod').getList(1, 4);
|
||||
|
||||
if (!records.items || records.items.length === 0) {
|
||||
log('jobInfoOutput', 'No records found', 'warning');
|
||||
setStatus('jobInfoStatus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
let output = `✓ Found ${records.items.length} records:\n\n`;
|
||||
|
||||
records.items.forEach((record, index) => {
|
||||
output += `[${index + 1}] ID: ${record.id}\n`;
|
||||
output += ` Title: ${record.title || record.name || 'N/A'}\n`;
|
||||
output += ` Status: ${record.status || 'N/A'}\n`;
|
||||
output += ` Created: ${new Date(record.created).toLocaleDateString()}\n\n`;
|
||||
});
|
||||
|
||||
log('jobInfoOutput', output, 'success');
|
||||
setStatus('jobInfoStatus', 'success');
|
||||
} catch (error) {
|
||||
log('jobInfoOutput', `✗ Failed to fetch records: ${error.message}`, 'error');
|
||||
setStatus('jobInfoStatus', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "mode1-auth-test",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "bun run backend/server.ts",
|
||||
"dev": "bun --watch backend/server.ts",
|
||||
"build": "vite build",
|
||||
"prebuild": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^5.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'frontend/dist',
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: 'frontend/auth.ts',
|
||||
name: 'AuthModule',
|
||||
fileName: () => 'auth.js',
|
||||
formats: ['umd']
|
||||
}
|
||||
},
|
||||
server: {
|
||||
middlewareMode: true
|
||||
}
|
||||
});
|
||||
@@ -11,10 +11,11 @@ const ORCHESTRATOR_PORT = 3006; // Orchestrator API runs here
|
||||
// Mode folder mappings - only Mode6Test exists currently
|
||||
const MODE_FOLDERS: Record<string, string> = {
|
||||
'Mode1': 'Mode1',
|
||||
'Mode1Svelte': 'Mode1Svelte',
|
||||
'Mode6Test': 'Mode6Test',
|
||||
};
|
||||
|
||||
let currentMode: string = 'Mode6Test';
|
||||
let currentMode: string = 'Mode1Svelte';
|
||||
let currentProcess: any = null;
|
||||
|
||||
// Helper: Read active mode from config file
|
||||
|
||||
Reference in New Issue
Block a user