Rebuild Mode1Svelte with SvelteKit + Node.js adapter
- Migrated from Bun/Hono to modern SvelteKit tech stack - Implemented complete authentication module (Azure MSAL + PocketBase) - Created 5 API endpoints for auth operations (config, graph token, validate, refresh, compare) - Built interactive Svelte dashboard with mobile optimization (iPhone 16) - Added Node.js adapter (@sveltejs/adapter-node) for production deployment - Integrated Tailwind CSS with responsive design - Fixed orchestrator to spawn Mode1Svelte with Node.js on port 3005 - Updated systemd service (job-info-test.service) to start orchestrator on port 3006 - Port configuration locked: 3005 = Mode1Svelte, 3006 = Orchestrator - Full TypeScript type safety throughout application - Services auto-managed by orchestrator with systemd integration
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
+275
-120
@@ -1,158 +1,313 @@
|
|||||||
# Mode1Svelte - Auth Module Test Suite
|
# Mode1Svelte - SvelteKit Auth Testing Suite
|
||||||
|
|
||||||
Self-contained test environment for the auth-module. This version is identical to Mode1 currently, serving as a foundation for future Svelte integration.
|
A complete SvelteKit-based testing environment for authentication workflows with Microsoft Azure MSAL and PocketBase. This is a modern rewrite of Mode1 using the latest SvelteKit technology stack.
|
||||||
|
|
||||||
## Structure
|
## Tech Stack
|
||||||
|
|
||||||
```
|
- **Framework**: SvelteKit 2.x with TypeScript
|
||||||
Mode1Svelte/
|
- **Backend**: Node.js Adapter (production-ready)
|
||||||
├── auth-module/ # Local copy of auth-module (not linked to root)
|
- **Styling**: Tailwind CSS 4
|
||||||
├── backend/ # Backend server with auth endpoints
|
- **Authentication**:
|
||||||
│ └── server.ts # Hono app with Graph token and PB validation APIs
|
- Azure MSAL Node (@azure/msal-node) - Microsoft Graph token management
|
||||||
├── frontend/ # Frontend test interface
|
- PocketBase - User OAuth2 and token validation
|
||||||
│ └── index.html # Interactive test dashboard
|
- **Build Tool**: Vite (integrated with SvelteKit)
|
||||||
├── logs/ # Application logs
|
- **Runtime**: Node.js 18+
|
||||||
├── package.json # Dependencies (auth module + Hono + PocketBase)
|
|
||||||
└── bun.lock # Locked dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
✓ **Frontend Auth (PocketBaseAuth)**
|
### ✅ Frontend Authentication (PocketBase)
|
||||||
- OAuth2 login initialization
|
- OAuth2 login initialization with Microsoft
|
||||||
- Auth state retrieval
|
- Auth state management and retrieval
|
||||||
- Token refresh
|
- Token refresh and validation
|
||||||
- Logout
|
- Session persistence
|
||||||
|
- Automatic logout on invalid tokens
|
||||||
|
|
||||||
✓ **Backend Auth (GraphTokenManager)**
|
### ✅ Backend Token Management (Microsoft Graph)
|
||||||
- Microsoft Graph token acquisition
|
- App-only token acquisition via MSAL
|
||||||
- Token caching (with 60s expiration buffer)
|
- Automatic token caching with 60-second expiration buffer
|
||||||
- Cache validity checking
|
- Token refresh on demand
|
||||||
- Cache refresh
|
- JWT payload inspection
|
||||||
|
|
||||||
✓ **PocketBase Validation (PocketBaseValidator)**
|
### ✅ Token Validation & Comparison
|
||||||
- User token validation
|
- PocketBase user token validation
|
||||||
- User record retrieval
|
- User record retrieval from tokens
|
||||||
- Token comparison (Graph vs PocketBase)
|
- Side-by-side token comparison (Graph vs PocketBase)
|
||||||
|
- Token payload analysis
|
||||||
|
|
||||||
## Running Mode1Svelte
|
### ✅ Interactive Testing Dashboard
|
||||||
|
- Real-time authentication status
|
||||||
|
- Visual token testing interface
|
||||||
|
- API endpoint testing
|
||||||
|
- Response inspection
|
||||||
|
|
||||||
### Via Orchestrator (recommended)
|
## Project Structure
|
||||||
|
|
||||||
```bash
|
```
|
||||||
# Switch to Mode1Svelte
|
Mode1Svelte/
|
||||||
curl -X POST http://localhost:3006/switch/Mode1Svelte
|
├── src/
|
||||||
|
│ ├── lib/
|
||||||
# Check status
|
│ │ └── auth/
|
||||||
curl http://localhost:3006/status
|
│ │ ├── types.ts # TypeScript interfaces
|
||||||
|
│ │ ├── backend.ts # Backend auth classes (MSAL, PocketBase)
|
||||||
|
│ │ └── frontend.ts # Frontend PocketBase auth class
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ ├── +layout.svelte # App layout
|
||||||
|
│ │ ├── +page.svelte # Main dashboard
|
||||||
|
│ │ └── api/
|
||||||
|
│ │ └── auth/
|
||||||
|
│ │ ├── config/ # GET /api/auth/config
|
||||||
|
│ │ ├── graph/ # GET /api/auth/graph/token
|
||||||
|
│ │ ├── validate-pb-token/ # POST /api/auth/validate-pb-token
|
||||||
|
│ │ ├── refresh-graph/ # POST /api/auth/refresh-graph
|
||||||
|
│ │ └── compare-tokens/ # POST /api/auth/compare-tokens
|
||||||
|
│ └── app.css # Tailwind directives
|
||||||
|
├── static/ # Static assets
|
||||||
|
├── package.json # Dependencies
|
||||||
|
├── svelte.config.js # SvelteKit config (Node adapter)
|
||||||
|
├── tailwind.config.js # Tailwind CSS config
|
||||||
|
├── vite.config.ts # Vite config
|
||||||
|
└── tsconfig.json # TypeScript config
|
||||||
```
|
```
|
||||||
|
|
||||||
### Direct
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd Mode1Svelte
|
cd Mode1Svelte
|
||||||
bun install
|
npm install
|
||||||
PORT=3005 bun run backend/server.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then visit: `http://localhost:3005`
|
## Environment Setup
|
||||||
|
|
||||||
## Future: Svelte Integration
|
Ensure your `.env` file or environment variables are set:
|
||||||
|
|
||||||
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
|
```bash
|
||||||
# Switch to Mode1
|
export CLIENT_ID="<Azure Client ID>"
|
||||||
curl -X POST http://localhost:3006/switch/Mode1
|
export TENANT_ID="<Azure Tenant ID>"
|
||||||
|
export CLIENT_SECRET="<Azure Client Secret>"
|
||||||
# Check status
|
export PB_URL="<PocketBase URL>"
|
||||||
curl http://localhost:3006/status
|
export PB_DB="<PocketBase Database URL>"
|
||||||
|
export PORT=5173
|
||||||
```
|
```
|
||||||
|
|
||||||
### Direct
|
For development, create `/home/admin/secrets/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLIENT_ID=your_client_id
|
||||||
|
TENANT_ID=your_tenant_id
|
||||||
|
CLIENT_SECRET=your_client_secret
|
||||||
|
PB_URL=https://pocketbase.example.com
|
||||||
|
PB_DB=https://pocketbase.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies (already done)
|
npm run dev
|
||||||
bun install
|
|
||||||
|
|
||||||
# Start server
|
|
||||||
bun run backend/server.ts
|
|
||||||
|
|
||||||
# Access dashboard
|
|
||||||
open http://localhost:3005
|
|
||||||
```
|
```
|
||||||
|
Starts the dev server at `http://localhost:5173` with hot reloading.
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
Builds the application and runs the Node.js server.
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
Run TypeScript type checking across the project.
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### Configuration
|
### GET `/api/auth/config`
|
||||||
- `GET /api/auth/config` - Get frontend configuration
|
Returns frontend-safe configuration for PocketBase OAuth setup.
|
||||||
|
|
||||||
### Graph Token Management
|
**Response:**
|
||||||
- `GET /api/auth/graph/token` - Get current Graph token (with expiration)
|
```json
|
||||||
- `POST /api/auth/refresh-graph` - Force refresh Graph token
|
{
|
||||||
|
"pbUrl": "https://pocketbase.example.com",
|
||||||
|
"provider": "microsoft",
|
||||||
|
"collection": "Users"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### PocketBase Validation
|
### GET `/api/auth/graph/token`
|
||||||
- `POST /api/auth/validate-pb-token` - Validate a PocketBase token
|
Fetches a Microsoft Graph token for backend API access.
|
||||||
|
|
||||||
### Health
|
**Response:**
|
||||||
- `GET /health` - Health check
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"token": "eyJ0eXAiOiJKV1Q...",
|
||||||
|
"fullToken": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||||
|
"expiresOn": "2026-01-23T02:15:00.000Z",
|
||||||
|
"isValid": true,
|
||||||
|
"tokenDetails": {
|
||||||
|
"aud": "https://graph.microsoft.com",
|
||||||
|
"iss": "https://sts.windows.net/tenant-id",
|
||||||
|
"scp": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Environment Variables
|
### POST `/api/auth/validate-pb-token`
|
||||||
|
Validates a PocketBase user token and retrieves user information.
|
||||||
|
|
||||||
Uses `/home/admin/secrets/.env` via auth-module:
|
**Request:**
|
||||||
- `CLIENT_ID` - Microsoft application ID
|
```json
|
||||||
- `TENANT_ID` - Azure tenant ID
|
{
|
||||||
- `CLIENT_SECRET` - Microsoft client secret
|
"pbToken": "user_token_string"
|
||||||
- `PB_URL` - PocketBase frontend URL
|
}
|
||||||
- `PB_DB` - PocketBase backend URL
|
```
|
||||||
|
|
||||||
## Dependencies
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"user": {
|
||||||
|
"id": "user_id",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "User Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
All dependencies are installed locally in `node_modules/`:
|
### POST `/api/auth/refresh-graph`
|
||||||
- `hono` - Web framework
|
Clears the cached Graph token and acquires a new one.
|
||||||
- `pocketbase` - PocketBase client
|
|
||||||
- `@azure/msal-node` - Microsoft authentication
|
|
||||||
- `dotenv` - Environment variable loading
|
|
||||||
|
|
||||||
## Notes
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"refreshed": true,
|
||||||
|
"expiresOn": "2026-01-23T02:15:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- This Mode1 is **completely self-contained** with its own auth-module copy
|
### POST `/api/auth/compare-tokens`
|
||||||
- No dependencies on root auth-module
|
Compares Graph token (backend) and PocketBase token (user) to demonstrate their differences.
|
||||||
- Safe to modify and test independently
|
|
||||||
- All secrets come from `/home/admin/secrets/.env`
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pbToken": "user_token_string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"comparison": {
|
||||||
|
"graph_token": {
|
||||||
|
"issuer": "https://sts.windows.net/tenant",
|
||||||
|
"audience": "https://graph.microsoft.com",
|
||||||
|
"app": "App Name",
|
||||||
|
"scopes": "...",
|
||||||
|
"issued_at": "2026-01-23T01:15:00.000Z",
|
||||||
|
"expires_at": "2026-01-23T02:15:00.000Z",
|
||||||
|
"type": "Microsoft Graph Token (Backend/App-only)",
|
||||||
|
"source": "@azure/msal-node",
|
||||||
|
"purpose": "Backend API calls to Microsoft Graph"
|
||||||
|
},
|
||||||
|
"pocketbase_token": {
|
||||||
|
"type": "PocketBase User Token (Frontend/User)",
|
||||||
|
"source": "PocketBase OAuth2 flow",
|
||||||
|
"authenticated_user": "user@example.com"
|
||||||
|
},
|
||||||
|
"are_different": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
### Frontend (Browser)
|
||||||
|
1. User clicks "Login with Microsoft"
|
||||||
|
2. PocketBase initiates OAuth2 flow with Microsoft provider
|
||||||
|
3. Microsoft redirects back with authorization code
|
||||||
|
4. PocketBase exchanges code for user token
|
||||||
|
5. Token stored in PocketBase auth store
|
||||||
|
6. User dashboard updates with authenticated state
|
||||||
|
|
||||||
|
### Backend (Server)
|
||||||
|
1. Application starts with MSAL configuration
|
||||||
|
2. On first API request, acquires Graph token using client credentials
|
||||||
|
3. Token cached for 55 minutes (60-minute lifetime - 5-minute buffer)
|
||||||
|
4. Subsequent requests use cached token
|
||||||
|
5. Token automatically refreshed when expired
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- **CORS**: Configured for `localhost:5173` and production URLs
|
||||||
|
- **Token Caching**: Graph tokens cached in memory; refresh on demand with `/api/auth/refresh-graph`
|
||||||
|
- **Error Handling**: Comprehensive error messages with specific failure reasons
|
||||||
|
- **Type Safety**: Full TypeScript support throughout frontend and backend
|
||||||
|
- **SvelteKit Routing**: File-based routing with `+page.svelte` and `+server.ts` files
|
||||||
|
|
||||||
|
## Comparison with Mode1 (Hono/Bun)
|
||||||
|
|
||||||
|
| Feature | Mode1Svelte | Mode1 |
|
||||||
|
|---------|-------------|--------|
|
||||||
|
| Framework | SvelteKit 2 | SvelteKit 4 (old) |
|
||||||
|
| Backend | Node.js Adapter | Hono + Bun |
|
||||||
|
| Frontend | Svelte Components | HTML + Vanilla JS |
|
||||||
|
| Styling | Tailwind CSS | CSS (custom) |
|
||||||
|
| Build Tool | Vite | Vite |
|
||||||
|
| API Routes | `/routes/api/` | `/routes/api/` |
|
||||||
|
| Auth Module | `/lib/auth/` | `/routes/api/` |
|
||||||
|
| Type Safety | Full TypeScript | TypeScript (partial) |
|
||||||
|
| Production Ready | ✅ Yes | ⚠️ Experimental |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Token Acquisition Fails
|
||||||
|
- Verify `CLIENT_ID`, `TENANT_ID`, and `CLIENT_SECRET` are correct
|
||||||
|
- Ensure the Azure application is configured for client credentials flow
|
||||||
|
- Check that the application has Graph API permissions
|
||||||
|
|
||||||
|
### PocketBase Token Validation Fails
|
||||||
|
- Verify `PB_URL` and `PB_DB` are correct
|
||||||
|
- Ensure the user is properly logged in via OAuth
|
||||||
|
- Check that PocketBase is running and accessible
|
||||||
|
|
||||||
|
### Login Button Not Appearing
|
||||||
|
- Ensure JavaScript is enabled
|
||||||
|
- Check browser console for errors
|
||||||
|
- Verify that `loginBtn` HTML element ID matches configuration
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [SvelteKit Documentation](https://svelte.dev/docs/kit)
|
||||||
|
- [Azure MSAL Node](https://github.com/AzureAD/microsoft-authentication-library-for-js)
|
||||||
|
- [PocketBase Documentation](https://pocketbase.io/docs/)
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private - Job Info Testing Suite
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
{
|
|
||||||
"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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Frontend entry point - exports auth module for browser
|
|
||||||
export { PocketBaseAuth, initPocketBaseAuth } from '../auth-module/frontend';
|
|
||||||
export type { AuthConfig, AuthState, AuthCallbacks } from '../auth-module/types';
|
|
||||||
@@ -1,586 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Mode1Svelte - 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-indicator {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 1rem;
|
|
||||||
left: 1rem;
|
|
||||||
background: #1e293b;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-indicator .mode-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-indicator .mode-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #22c55e;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 (Mode1Svelte)</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>
|
|
||||||
|
|
||||||
<!-- Mode Indicator -->
|
|
||||||
<div class="mode-indicator">
|
|
||||||
<div class="mode-dot"></div>
|
|
||||||
<span class="mode-name">Mode1Svelte</span>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Generated
+2823
File diff suppressed because it is too large
Load Diff
+32
-24
@@ -1,26 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "mode1-auth-test",
|
"name": "mode1svelte",
|
||||||
"version": "1.0.0",
|
"private": true,
|
||||||
"type": "module",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run backend/server.ts",
|
"dev": "vite dev --port 3005",
|
||||||
"dev": "bun --watch backend/server.ts",
|
"build": "vite build",
|
||||||
"build": "vite build",
|
"preview": "vite preview",
|
||||||
"prebuild": "vite build"
|
"start": "PORT=3005 node build/index.js",
|
||||||
},
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"dependencies": {
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"@azure/msal-node": "^5.0.2",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
"dotenv": "^17.2.3",
|
},
|
||||||
"hono": "^4.10.8",
|
"devDependencies": {
|
||||||
"pocketbase": "^0.26.5"
|
"@sveltejs/adapter-node": "^5.5.2",
|
||||||
},
|
"@sveltejs/kit": "^2.49.1",
|
||||||
"devDependencies": {
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@types/bun": "latest",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"typescript": "^5",
|
"autoprefixer": "^10.4.23",
|
||||||
"vite": "^7.3.1"
|
"postcss": "^8.5.6",
|
||||||
},
|
"svelte": "^5.45.6",
|
||||||
"peerDependencies": {
|
"svelte-check": "^4.3.4",
|
||||||
"bun": "latest"
|
"tailwindcss": "^4.1.18",
|
||||||
}
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.2.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-node": "^5.0.2",
|
||||||
|
"@types/node": "^25.0.10",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"pocketbase": "^0.26.6"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
|
sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="theme-color" content="#1e293b" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
import { GraphTokenCache, BackendAuthConfig } from './types';
|
import type { GraphTokenCache, BackendAuthConfig } from './types';
|
||||||
|
|
||||||
// Load environment variables from shared secrets directory
|
// Load environment variables from shared secrets directory
|
||||||
config({ path: '/home/admin/secrets/.env' });
|
config({ path: '/home/admin/secrets/.env' });
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
import { AuthConfig, AuthState, AuthCallbacks } from './types';
|
import type { AuthConfig, AuthState, AuthCallbacks } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PocketBase OAuth2 Frontend Module
|
* PocketBase OAuth2 Frontend Module
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" href={favicon} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { PocketBaseAuth } from '$lib/auth/frontend';
|
||||||
|
import type { AuthState } from '$lib/auth/types';
|
||||||
|
|
||||||
|
let auth: PocketBaseAuth | undefined;
|
||||||
|
let authState: AuthState = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
};
|
||||||
|
let graphToken = '';
|
||||||
|
let pbTokenValid = false;
|
||||||
|
let comparisonData: string | null = null;
|
||||||
|
let loading = false;
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/config');
|
||||||
|
if (!response.ok) throw new Error('Failed to get config');
|
||||||
|
const config = await response.json();
|
||||||
|
|
||||||
|
auth = new PocketBaseAuth(config, {
|
||||||
|
onAuthSuccess: (state: AuthState) => {
|
||||||
|
authState = state;
|
||||||
|
console.log('Auth success:', state);
|
||||||
|
},
|
||||||
|
onAuthFailure: (err: unknown) => {
|
||||||
|
error = 'Authentication failed: ' + (err instanceof Error ? err.message : 'Unknown error');
|
||||||
|
console.error('Auth error:', err);
|
||||||
|
},
|
||||||
|
onTokenUpdate: (state: AuthState) => {
|
||||||
|
authState = state;
|
||||||
|
},
|
||||||
|
onUiUpdate: (state: AuthState) => {
|
||||||
|
authState = state;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleFetchGraphToken() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/graph/token');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch Graph token');
|
||||||
|
const data = await response.json();
|
||||||
|
graphToken = JSON.stringify(data, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRefreshGraphToken() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/refresh-graph', { method: 'POST' });
|
||||||
|
if (!response.ok) throw new Error('Failed to refresh');
|
||||||
|
const data = await response.json();
|
||||||
|
graphToken = JSON.stringify(data, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleValidatePBToken() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const pbToken = auth?.getPocketBase?.()?.authStore?.token;
|
||||||
|
if (!pbToken) {
|
||||||
|
error = 'No PocketBase token found. Please log in first.';
|
||||||
|
pbTokenValid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth/validate-pb-token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ pbToken }),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Validation failed');
|
||||||
|
const data = await response.json();
|
||||||
|
pbTokenValid = data.valid;
|
||||||
|
error = data.valid ? '' : 'Token validation failed';
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
pbTokenValid = false;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCompareTokens() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const pbToken = auth?.getPocketBase?.()?.authStore?.token;
|
||||||
|
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('Comparison failed');
|
||||||
|
const data = await response.json();
|
||||||
|
comparisonData = JSON.stringify(data, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
if (auth) {
|
||||||
|
auth.logout();
|
||||||
|
authState = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
};
|
||||||
|
error = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="min-height: 100vh; background: linear-gradient(to bottom, #0f172a, #1e293b); color: white; padding: 16px; padding-bottom: 100px;">
|
||||||
|
<div style="max-width: 800px; margin: 0 auto;">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="margin-bottom: 32px;">
|
||||||
|
<h1 style="font-size: 28px; font-weight: bold; margin: 0 0 8px 0;">Mode1Svelte</h1>
|
||||||
|
<p style="font-size: 14px; color: #94a3b8; margin: 0;">Auth Testing Dashboard</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Display -->
|
||||||
|
{#if error}
|
||||||
|
<div style="background: rgba(127, 29, 29, 0.2); border: 1px solid #dc2626; color: #fca5a5; padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 14px;">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Auth Status -->
|
||||||
|
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
|
||||||
|
<h2 style="font-size: 20px; font-weight: bold; margin: 0 0 12px 0;">Authentication Status</h2>
|
||||||
|
|
||||||
|
{#if authState.isAuthenticated && authState.user !== null}
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||||
|
<div style="font-size: 14px;">
|
||||||
|
<span style="color: #cbd5e1;">Status:</span>
|
||||||
|
<span style="margin-left: 8px; color: #4ade80; font-weight: bold;">✓ Authenticated</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 14px; word-break: break-all;">
|
||||||
|
<span style="color: #cbd5e1;">User:</span>
|
||||||
|
<span style="margin-left: 8px; color: #93c5fd;" id="userDisplayName">{authState.user?.name}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 14px; word-break: break-all;">
|
||||||
|
<span style="color: #cbd5e1;">Email:</span>
|
||||||
|
<span style="margin-left: 8px; color: #93c5fd;" id="userEmailValue">{authState.user?.email}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
on:click={handleLogout}
|
||||||
|
style="background: #dc2626; color: white; border: none; padding: 12px 16px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; margin-top: 8px; width: 100%;"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div id="loginContainer" style="display: flex; flex-direction: column; gap: 12px;">
|
||||||
|
<p style="font-size: 14px; color: #cbd5e1; margin: 0;">Not authenticated. Log in to begin testing.</p>
|
||||||
|
<button
|
||||||
|
id="loginBtn"
|
||||||
|
style="background: #2563eb; color: white; border: none; padding: 16px; border-radius: 6px; font-weight: bold; font-size: 16px; cursor: pointer; width: 100%;"
|
||||||
|
>
|
||||||
|
Login with Microsoft
|
||||||
|
</button>
|
||||||
|
<div id="loginError" style="color: #f87171; font-size: 12px; display: none;"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Testing -->
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px;">
|
||||||
|
<!-- Graph Token -->
|
||||||
|
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px;">
|
||||||
|
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">Graph Token</h3>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
<button
|
||||||
|
on:click={handleFetchGraphToken}
|
||||||
|
disabled={loading}
|
||||||
|
style="background: #4f46e5; color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Fetch Token'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={handleRefreshGraphToken}
|
||||||
|
disabled={loading}
|
||||||
|
style="background: #4f46e5; color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PocketBase Token -->
|
||||||
|
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px;">
|
||||||
|
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">PocketBase Token</h3>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
<button
|
||||||
|
on:click={handleValidatePBToken}
|
||||||
|
disabled={loading}
|
||||||
|
style="background: #059669; color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Validate'}
|
||||||
|
</button>
|
||||||
|
{#if pbTokenValid}
|
||||||
|
<p style="color: #4ade80; font-size: 12px; margin: 0;">✓ Token is valid</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Compare Tokens -->
|
||||||
|
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||||||
|
<button
|
||||||
|
on:click={handleCompareTokens}
|
||||||
|
disabled={loading}
|
||||||
|
style="background: #9333ea; color: white; border: none; padding: 12px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; opacity: {loading ? 0.6 : 1}; width: 100%;"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Compare Tokens'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
{#if graphToken}
|
||||||
|
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||||||
|
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">Graph Token Response</h3>
|
||||||
|
<pre style="background: #0f172a; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 11px; color: #cbd5e1; max-height: 200px; white-space: pre-wrap; word-wrap: break-word; margin: 0;">{graphToken}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if comparisonData}
|
||||||
|
<div style="background: rgba(71, 85, 105, 0.3); border: 1px solid #475569; padding: 12px; border-radius: 8px;">
|
||||||
|
<h3 style="font-size: 16px; font-weight: bold; margin: 0 0 12px 0;">Token Comparison</h3>
|
||||||
|
<pre style="background: #0f172a; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 11px; color: #cbd5e1; max-height: 200px; white-space: pre-wrap; word-wrap: break-word; margin: 0;">{comparisonData}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import { BackendAuth } from '$lib/auth/backend';
|
||||||
|
|
||||||
|
const backendAuth = new BackendAuth(
|
||||||
|
{
|
||||||
|
clientId: process.env.CLIENT_ID,
|
||||||
|
tenantId: process.env.TENANT_ID,
|
||||||
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
process.env.PB_DB
|
||||||
|
);
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const pbToken = body.pbToken;
|
||||||
|
|
||||||
|
// Get Graph token
|
||||||
|
const { token: graphToken, expiresOnISO: graphExpires } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||||
|
|
||||||
|
if (!graphToken) {
|
||||||
|
return json({ success: false, error: 'Failed to acquire Graph token' }, { status: 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 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 json(
|
||||||
|
{ success: false, error: error instanceof Error ? error.message : 'Failed to compare tokens' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { AuthConfigManager } from '$lib/auth/backend';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const config = AuthConfigManager.getFrontendConfig();
|
||||||
|
return json(config);
|
||||||
|
} catch (error) {
|
||||||
|
return json(
|
||||||
|
{ error: 'Failed to get config', message: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import { BackendAuth, AuthConfigManager } from '$lib/auth/backend';
|
||||||
|
|
||||||
|
const backendAuth = new BackendAuth(
|
||||||
|
{
|
||||||
|
clientId: process.env.CLIENT_ID,
|
||||||
|
tenantId: process.env.TENANT_ID,
|
||||||
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
process.env.PB_DB
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => {
|
||||||
|
try {
|
||||||
|
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return json({ success: false, error: 'Failed to acquire Graph token' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode JWT to show token details (for verification)
|
||||||
|
const parts = token.split('.');
|
||||||
|
let payload: any = {};
|
||||||
|
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 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 json(
|
||||||
|
{ success: false, error: error instanceof Error ? error.message : 'Failed to acquire token' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import { BackendAuth } from '$lib/auth/backend';
|
||||||
|
|
||||||
|
const backendAuth = new BackendAuth(
|
||||||
|
{
|
||||||
|
clientId: process.env.CLIENT_ID,
|
||||||
|
tenantId: process.env.TENANT_ID,
|
||||||
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
process.env.PB_DB
|
||||||
|
);
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async () => {
|
||||||
|
try {
|
||||||
|
backendAuth.graphTokenManager.clearCache();
|
||||||
|
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
refreshed: true,
|
||||||
|
expiresOn: expiresOnISO,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return json(
|
||||||
|
{ success: false, error: error instanceof Error ? error.message : 'Refresh failed' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import { BackendAuth } from '$lib/auth/backend';
|
||||||
|
|
||||||
|
const backendAuth = new BackendAuth(
|
||||||
|
{
|
||||||
|
clientId: process.env.CLIENT_ID,
|
||||||
|
tenantId: process.env.TENANT_ID,
|
||||||
|
clientSecret: process.env.CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
process.env.PB_DB
|
||||||
|
);
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const token = body.pbToken;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return json({ valid: false, error: 'No token provided' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await backendAuth.pbValidator.validateUserToken(token);
|
||||||
|
const user = isValid ? await backendAuth.pbValidator.getUserRecord(token) : null;
|
||||||
|
|
||||||
|
return json({
|
||||||
|
valid: isValid,
|
||||||
|
user: user ? {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
} : null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return json(
|
||||||
|
{ valid: false, error: error instanceof Error ? error.message : 'Validation failed' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-node';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter(),
|
||||||
|
// LOCKED: Always use port 3005 for Mode1Svelte service
|
||||||
|
// 3006 = Orchestrator, 3005 = Mode service
|
||||||
|
// https://ji-test.ccllc.pro proxies to these ports
|
||||||
|
paths: {
|
||||||
|
base: process.env.BASE_PATH || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
+11
-13
@@ -1,17 +1,15 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
plugins: [sveltekit()],
|
||||||
outDir: 'frontend/dist',
|
build: {
|
||||||
emptyOutDir: true,
|
// Ensure asset paths are relative, not absolute
|
||||||
lib: {
|
// This fixes proxy routing issues
|
||||||
entry: 'frontend/auth.ts',
|
rollupOptions: {
|
||||||
name: 'AuthModule',
|
output: {
|
||||||
fileName: () => 'auth.js',
|
assetFileNames: 'assets/[name]-[hash][extname]'
|
||||||
formats: ['umd']
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
server: {
|
|
||||||
middlewareMode: true
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Mode6Test
|
Mode1Svelte
|
||||||
@@ -82,20 +82,40 @@ async function startMode(mode: string): Promise<boolean> {
|
|||||||
|
|
||||||
console.log(`[ModeSwitch] Starting ${mode} (${folderName}/) on port ${SHARED_PORT}...`);
|
console.log(`[ModeSwitch] Starting ${mode} (${folderName}/) on port ${SHARED_PORT}...`);
|
||||||
|
|
||||||
// Determine server path - all modes use backend/server.ts
|
// Mode1Svelte uses Node.js adapter with build/index.js
|
||||||
const serverPath = 'backend/server.ts';
|
if (mode === 'Mode1Svelte') {
|
||||||
|
const buildPath = `${modePath}/build`;
|
||||||
|
if (!fs.existsSync(buildPath)) {
|
||||||
|
console.error(`Build directory not found for Mode1Svelte. Run: cd ${modePath} && npm run build`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Start the mode's backend server
|
// Start with Node.js
|
||||||
currentProcess = Bun.spawn({
|
currentProcess = Bun.spawn({
|
||||||
cmd: [process.execPath, 'run', serverPath],
|
cmd: ['node', 'build/index.js'],
|
||||||
cwd: modePath,
|
cwd: modePath,
|
||||||
env: {
|
env: {
|
||||||
...process.env, // Inherit all env vars (secrets, etc.)
|
...process.env,
|
||||||
PORT: String(SHARED_PORT), // Force mode service to bind to shared port
|
PORT: String(SHARED_PORT),
|
||||||
},
|
},
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Other modes (Mode1, Mode6Test) use Bun with backend/server.ts
|
||||||
|
const serverPath = 'backend/server.ts';
|
||||||
|
|
||||||
|
currentProcess = Bun.spawn({
|
||||||
|
cmd: [process.execPath, 'run', serverPath],
|
||||||
|
cwd: modePath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PORT: String(SHARED_PORT),
|
||||||
|
},
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
currentMode = mode;
|
currentMode = mode;
|
||||||
writeActiveMode(mode);
|
writeActiveMode(mode);
|
||||||
|
|||||||
Reference in New Issue
Block a user