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:
2026-01-22 05:53:15 +00:00
parent 1fdc87f7f4
commit 5a4a3c51e9
12 changed files with 1967 additions and 1 deletions
+158
View File
@@ -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`
+248
View File
@@ -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
+204
View File
@@ -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();
}
}
+250
View File
@@ -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;
}
+62
View File
@@ -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;
}
+226
View File
@@ -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,
};
+226
View File
@@ -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=="],
}
}
+3
View File
@@ -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';
+545
View File
@@ -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>
+26
View File
@@ -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"
}
}
+17
View File
@@ -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
}
});