513e975530
- Replace popup OAuth with SDK authWithOAuth2 realtime channel (urlCallback) so login works in Edge app mode without navigating the main window - Remove manual redirect/sessionStorage OAuth flow that broke in Edge app mode - Serve PocketBase config (URL, collection, provider) from server endpoint backed by env vars instead of hardcoded localhost - Fix /api/submit: target Notes collection with correct field mapping (body_plain, body_html, title, type, email, Username, userId, job_note, Job_Number) instead of Job_Info_Prod with wrong field names - Remove localhost fallbacks from auth-module backend and frontend
172 lines
4.5 KiB
TypeScript
172 lines
4.5 KiB
TypeScript
import { ConfidentialClientApplication } from '@azure/msal-node';
|
|
import PocketBase from 'pocketbase';
|
|
import { GraphTokenCache, BackendAuthConfig } from './types';
|
|
|
|
/**
|
|
* Microsoft Graph Token Management (Backend)
|
|
* Handles token acquisition and caching for backend Graph API calls
|
|
*/
|
|
export class GraphTokenManager {
|
|
private cca: ConfidentialClientApplication;
|
|
private cache: GraphTokenCache | null = null;
|
|
private config: Required<BackendAuthConfig>;
|
|
|
|
constructor(config: BackendAuthConfig) {
|
|
this.config = {
|
|
clientId: config.clientId || process.env.CLIENT_ID || '',
|
|
tenantId: config.tenantId || process.env.TENANT_ID || '',
|
|
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
|
|
};
|
|
|
|
this.cca = new ConfidentialClientApplication({
|
|
auth: {
|
|
clientId: this.config.clientId,
|
|
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
|
|
clientSecret: this.config.clientSecret,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get Graph token (from cache or acquire new)
|
|
*/
|
|
async getToken(): Promise<string> {
|
|
const now = Date.now();
|
|
|
|
// Check cache validity (with 60s buffer for expiration)
|
|
if (this.cache && this.cache.expiresOn - 60000 > now) {
|
|
return this.cache.token;
|
|
}
|
|
|
|
try {
|
|
const result = await this.cca.acquireTokenByClientCredential({
|
|
scopes: ['https://graph.microsoft.com/.default'],
|
|
});
|
|
|
|
if (!result?.accessToken) {
|
|
throw new Error('Failed to acquire Graph token');
|
|
}
|
|
|
|
const expiresOn = result.expiresOn
|
|
? new Date(result.expiresOn).getTime()
|
|
: now + 55 * 60 * 1000; // Default 55 minutes
|
|
|
|
this.cache = {
|
|
token: result.accessToken,
|
|
expiresOn,
|
|
};
|
|
|
|
return result.accessToken;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
throw new Error(`Failed to acquire Graph token: ${message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get token with expiration info
|
|
*/
|
|
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
|
|
const token = await this.getToken();
|
|
const expiresOn = this.cache?.expiresOn || Date.now();
|
|
return {
|
|
token,
|
|
expiresOnISO: new Date(expiresOn).toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if token is cached and valid
|
|
*/
|
|
isTokenValid(): boolean {
|
|
if (!this.cache) return false;
|
|
const now = Date.now();
|
|
return this.cache.expiresOn - 60000 > now;
|
|
}
|
|
|
|
/**
|
|
* Clear cache (force refresh on next call)
|
|
*/
|
|
clearCache(): void {
|
|
this.cache = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PocketBase Token Validation (Backend)
|
|
* Validates and uses user PocketBase tokens
|
|
*/
|
|
export class PocketBaseValidator {
|
|
private pb: PocketBase;
|
|
|
|
constructor(pbUrl?: string) {
|
|
this.pb = new PocketBase(pbUrl || process.env.PB_URL || 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();
|
|
}
|
|
}
|