Compare commits
21 Commits
09fc949a06
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d3a98e6ce | |||
| e6777003a2 | |||
| e89f60f8a5 | |||
| 135087e096 | |||
| 7d8fd9f251 | |||
| 7b997dc354 | |||
| d8d53ece93 | |||
| c0d32d7658 | |||
| 5a4a3c51e9 | |||
| 1fdc87f7f4 | |||
| 3ff23cd2bd | |||
| 05f8e2e3b6 | |||
| bd7842799c | |||
| 0b76a4b119 | |||
| 42ff3e8f33 | |||
| 61dbbc457a | |||
| 3cc466c12d | |||
| 1322ace853 | |||
| 00b944c388 | |||
| 9e28d86610 | |||
| 3ed6c888ee |
@@ -1,73 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright 2025 aewing
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Mode1 - Auth Module Test Suite
|
||||
|
||||
Self-contained test environment for the auth-module. This directory includes its own copy of auth-module and all dependencies.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Mode1/
|
||||
├── auth-module/ # Local copy of auth-module (not linked to root)
|
||||
├── backend/ # Backend server with auth endpoints
|
||||
│ └── server.ts # Hono app with Graph token and PB validation APIs
|
||||
├── frontend/ # Frontend test interface
|
||||
│ └── index.html # Interactive test dashboard
|
||||
├── logs/ # Application logs
|
||||
├── package.json # Dependencies (auth module + Hono + PocketBase)
|
||||
└── bun.lock # Locked dependencies
|
||||
```
|
||||
|
||||
## Features Tested
|
||||
|
||||
✓ **Frontend Auth (PocketBaseAuth)**
|
||||
- OAuth2 login initialization
|
||||
- Auth state retrieval
|
||||
- Token refresh
|
||||
- Logout
|
||||
|
||||
✓ **Backend Auth (GraphTokenManager)**
|
||||
- Microsoft Graph token acquisition
|
||||
- Token caching (with 60s expiration buffer)
|
||||
- Cache validity checking
|
||||
- Cache refresh
|
||||
|
||||
✓ **PocketBase Validation (PocketBaseValidator)**
|
||||
- User token validation
|
||||
- User record retrieval
|
||||
|
||||
## Running Mode1
|
||||
|
||||
### Via Orchestrator (recommended)
|
||||
|
||||
```bash
|
||||
# Switch to Mode1
|
||||
curl -X POST http://localhost:3006/switch/Mode1
|
||||
|
||||
# Check status
|
||||
curl http://localhost:3006/status
|
||||
```
|
||||
|
||||
### Direct
|
||||
|
||||
```bash
|
||||
# Install dependencies (already done)
|
||||
bun install
|
||||
|
||||
# Start server
|
||||
bun run backend/server.ts
|
||||
|
||||
# Access dashboard
|
||||
open http://localhost:3005
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Configuration
|
||||
- `GET /api/auth/config` - Get frontend configuration
|
||||
|
||||
### Graph Token Management
|
||||
- `GET /api/auth/graph/token` - Get current Graph token (with expiration)
|
||||
- `POST /api/auth/refresh-graph` - Force refresh Graph token
|
||||
|
||||
### PocketBase Validation
|
||||
- `POST /api/auth/validate-pb-token` - Validate a PocketBase token
|
||||
|
||||
### Health
|
||||
- `GET /health` - Health check
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Uses `/home/admin/secrets/.env` via auth-module:
|
||||
- `CLIENT_ID` - Microsoft application ID
|
||||
- `TENANT_ID` - Azure tenant ID
|
||||
- `CLIENT_SECRET` - Microsoft client secret
|
||||
- `PB_URL` - PocketBase frontend URL
|
||||
- `PB_DB` - PocketBase backend URL
|
||||
|
||||
## Dependencies
|
||||
|
||||
All dependencies are installed locally in `node_modules/`:
|
||||
- `hono` - Web framework
|
||||
- `pocketbase` - PocketBase client
|
||||
- `@azure/msal-node` - Microsoft authentication
|
||||
- `dotenv` - Environment variable loading
|
||||
|
||||
## Notes
|
||||
|
||||
- This Mode1 is **completely self-contained** with its own auth-module copy
|
||||
- No dependencies on root auth-module
|
||||
- Safe to modify and test independently
|
||||
- All secrets come from `/home/admin/secrets/.env`
|
||||
@@ -0,0 +1,248 @@
|
||||
# Auth Module
|
||||
|
||||
Standalone authentication module for PocketBase OAuth2 + Microsoft Graph integration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Frontend**: PocketBase OAuth2 authentication with Microsoft provider
|
||||
- **Backend**: Microsoft Graph token management with automatic caching
|
||||
- **No dependencies on project-specific code** - Use in any project
|
||||
|
||||
## Installation
|
||||
|
||||
Copy the `auth-module` folder to your project:
|
||||
|
||||
```bash
|
||||
cp -r auth-module /path/to/your/project/
|
||||
```
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
bun add pocketbase @azure/msal-node
|
||||
```
|
||||
|
||||
## Frontend Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
**Important**: The frontend is browser-side code. It must receive `pbUrl` from the consuming application (not from the backend).
|
||||
|
||||
```typescript
|
||||
import { PocketBaseAuth } from './auth-module/frontend';
|
||||
|
||||
// In your project, load PB_URL from environment/config and pass it to frontend
|
||||
const auth = new PocketBaseAuth({
|
||||
pbUrl: process.env.PB_URL, // Required - must be provided by consuming application
|
||||
collection: 'Users',
|
||||
provider: 'microsoft',
|
||||
loginContainerId: 'loginContainer',
|
||||
userDisplayNameId: 'userDisplayName',
|
||||
userEmailId: 'userEmailValue',
|
||||
loginBtnId: 'loginBtn',
|
||||
loginErrorId: 'loginError',
|
||||
});
|
||||
```
|
||||
|
||||
### HTML Required
|
||||
|
||||
```html
|
||||
<!-- Login container (shown when not authenticated) -->
|
||||
<div id="loginContainer" class="hidden">
|
||||
<button id="loginBtn">Login with Microsoft</button>
|
||||
<div id="loginError" class="hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- User display (shown when authenticated) -->
|
||||
<div id="userDisplayName"></div>
|
||||
<div id="userEmailValue"></div>
|
||||
```
|
||||
|
||||
### Event Callbacks
|
||||
|
||||
```typescript
|
||||
const auth = new PocketBaseAuth(config, {
|
||||
onAuthSuccess: (state) => {
|
||||
console.log('Logged in:', state.user.email);
|
||||
// Initialize other systems here
|
||||
},
|
||||
onAuthFailure: (error) => {
|
||||
console.error('Login failed:', error);
|
||||
},
|
||||
onTokenUpdate: (state) => {
|
||||
console.log('Token refreshed');
|
||||
},
|
||||
onUiUpdate: (state) => {
|
||||
console.log('UI updated');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```typescript
|
||||
// Get current auth state
|
||||
const state = auth.getAuthState();
|
||||
console.log(state.isAuthenticated, state.user);
|
||||
|
||||
// Check token status
|
||||
const status = await auth.checkTokenStatus();
|
||||
|
||||
// Logout
|
||||
auth.logout();
|
||||
|
||||
// Get raw PocketBase instance if needed
|
||||
const pb = auth.getPocketBase();
|
||||
```
|
||||
|
||||
## Backend Usage
|
||||
|
||||
### Setup (server.ts)
|
||||
|
||||
```typescript
|
||||
import { GraphTokenManager, PocketBaseValidator, BackendAuth } from './auth-module/backend';
|
||||
|
||||
// Option 1: Use individual managers
|
||||
const graphMgr = new GraphTokenManager({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
});
|
||||
|
||||
const pbValidator = new PocketBaseValidator(process.env.PB_DB);
|
||||
|
||||
// Option 2: Use combined manager
|
||||
const backendAuth = new BackendAuth({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
}, process.env.PB_DB);
|
||||
```
|
||||
|
||||
### Get Graph Token
|
||||
|
||||
```typescript
|
||||
// Get token string
|
||||
const token = await graphMgr.getToken();
|
||||
|
||||
// Get token with expiration
|
||||
const { token, expiresOnISO } = await graphMgr.getTokenWithExpiry();
|
||||
|
||||
// Check if cached and valid
|
||||
if (graphMgr.isTokenValid()) {
|
||||
console.log('Using cached token');
|
||||
}
|
||||
|
||||
// Clear cache (force refresh)
|
||||
graphMgr.clearCache();
|
||||
```
|
||||
|
||||
### Validate PocketBase Token
|
||||
|
||||
```typescript
|
||||
// Validate token
|
||||
const isValid = await pbValidator.validateUserToken(token);
|
||||
|
||||
// Get user record
|
||||
const user = await pbValidator.getUserRecord(token);
|
||||
|
||||
// Get PocketBase instance
|
||||
const pb = pbValidator.getPocketBase();
|
||||
```
|
||||
|
||||
### Endpoint Example
|
||||
|
||||
```typescript
|
||||
app.get('/api/graph/status', async (c) => {
|
||||
try {
|
||||
const { token, expiresOnISO } = await graphMgr.getTokenWithExpiry();
|
||||
return c.json({
|
||||
active: true,
|
||||
expiresOnISO,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ active: false, error: error.message },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/submit', async (c) => {
|
||||
const body = await c.req.json();
|
||||
const pbToken = body.pbToken;
|
||||
|
||||
if (!pbToken) {
|
||||
return c.json({ error: 'Missing pbToken' }, 401);
|
||||
}
|
||||
|
||||
const isValid = await pbValidator.validateUserToken(pbToken);
|
||||
if (!isValid) {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
|
||||
// Token is valid, proceed with business logic
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required in `.env` file:
|
||||
|
||||
```env
|
||||
CLIENT_ID=your-microsoft-app-id
|
||||
TENANT_ID=your-azure-tenant-id
|
||||
CLIENT_SECRET=your-microsoft-client-secret
|
||||
PB_DB=https://your-pocketbase-instance.com
|
||||
PB_URL=https://your-pocketbase-instance.com
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Frontend Config Options
|
||||
|
||||
```typescript
|
||||
interface AuthConfig {
|
||||
pbUrl: string; // PocketBase URL (required)
|
||||
collection?: string; // Auth collection (default: Users)
|
||||
provider?: string; // OAuth provider (default: microsoft)
|
||||
loginContainerId?: string; // ID of login container element
|
||||
userDisplayNameId?: string; // ID of user name display element
|
||||
userEmailId?: string; // ID of user email display element
|
||||
loginBtnId?: string; // ID of login button element
|
||||
loginErrorId?: string; // ID of error message element
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Config Options
|
||||
|
||||
```typescript
|
||||
interface BackendAuthConfig {
|
||||
clientId?: string; // Microsoft app ID (or CLIENT_ID env var)
|
||||
tenantId?: string; // Azure tenant ID (or TENANT_ID env var)
|
||||
clientSecret?: string; // Client secret (or CLIENT_SECRET env var)
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
The module exports TypeScript types for type safety:
|
||||
|
||||
```typescript
|
||||
import { AuthState, AuthConfig, GraphTokenCache } from './auth-module/types';
|
||||
|
||||
const state: AuthState = auth.getAuthState();
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Frontend** manages user authentication only
|
||||
- **Backend** manages Graph API tokens (never exposed to client)
|
||||
- Tokens are cached in-memory on backend with automatic refresh
|
||||
- Module does not include project-specific features (alerts, etc.)
|
||||
- Each project can implement its own business logic on top
|
||||
|
||||
## License
|
||||
|
||||
Same as parent project
|
||||
@@ -0,0 +1,204 @@
|
||||
import { config } from 'dotenv';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { GraphTokenCache, BackendAuthConfig } from './types';
|
||||
|
||||
// Load environment variables from shared secrets directory
|
||||
config({ path: '/home/admin/secrets/.env' });
|
||||
|
||||
/**
|
||||
* Configuration Manager
|
||||
* Exposes frontend-safe configuration loaded from environment
|
||||
*/
|
||||
export class AuthConfigManager {
|
||||
/**
|
||||
* Get frontend configuration (safe to expose to browser)
|
||||
*/
|
||||
static getFrontendConfig() {
|
||||
return {
|
||||
pbUrl: process.env.PB_URL!,
|
||||
provider: 'microsoft',
|
||||
collection: 'Users',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all backend secrets (never expose to frontend)
|
||||
*/
|
||||
static getBackendConfig() {
|
||||
return {
|
||||
clientId: process.env.CLIENT_ID!,
|
||||
tenantId: process.env.TENANT_ID!,
|
||||
clientSecret: process.env.CLIENT_SECRET!,
|
||||
pbDb: process.env.PB_DB!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Graph Token Management (Backend)
|
||||
* Handles token acquisition and caching for backend Graph API calls
|
||||
*/
|
||||
export class GraphTokenManager {
|
||||
private cca: ConfidentialClientApplication;
|
||||
private cache: GraphTokenCache | null = null;
|
||||
private config: Required<BackendAuthConfig>;
|
||||
|
||||
constructor(config: BackendAuthConfig) {
|
||||
this.config = {
|
||||
clientId: config.clientId || process.env.CLIENT_ID || '',
|
||||
tenantId: config.tenantId || process.env.TENANT_ID || '',
|
||||
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
|
||||
};
|
||||
|
||||
this.cca = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId: this.config.clientId,
|
||||
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
|
||||
clientSecret: this.config.clientSecret,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Graph token (from cache or acquire new)
|
||||
*/
|
||||
async getToken(): Promise<string> {
|
||||
const now = Date.now();
|
||||
|
||||
// Check cache validity (with 60s buffer for expiration)
|
||||
if (this.cache && this.cache.expiresOn - 60000 > now) {
|
||||
return this.cache.token;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.cca.acquireTokenByClientCredential({
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
});
|
||||
|
||||
if (!result?.accessToken) {
|
||||
throw new Error('Failed to acquire Graph token');
|
||||
}
|
||||
|
||||
const expiresOn = result.expiresOn
|
||||
? new Date(result.expiresOn).getTime()
|
||||
: now + 55 * 60 * 1000; // Default 55 minutes
|
||||
|
||||
this.cache = {
|
||||
token: result.accessToken,
|
||||
expiresOn,
|
||||
};
|
||||
|
||||
return result.accessToken;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new Error(`Failed to acquire Graph token: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token with expiration info
|
||||
*/
|
||||
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
|
||||
const token = await this.getToken();
|
||||
const expiresOn = this.cache?.expiresOn || Date.now();
|
||||
return {
|
||||
token,
|
||||
expiresOnISO: new Date(expiresOn).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is cached and valid
|
||||
*/
|
||||
isTokenValid(): boolean {
|
||||
if (!this.cache) return false;
|
||||
const now = Date.now();
|
||||
return this.cache.expiresOn - 60000 > now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache (force refresh on next call)
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PocketBase Token Validation (Backend)
|
||||
* Validates and uses user PocketBase tokens
|
||||
*/
|
||||
export class PocketBaseValidator {
|
||||
private pb: PocketBase;
|
||||
|
||||
constructor(pbUrl?: string) {
|
||||
this.pb = new PocketBase(pbUrl || process.env.PB_DB!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user token and validate it
|
||||
*/
|
||||
async validateUserToken(token: string): Promise<boolean> {
|
||||
try {
|
||||
this.pb.authStore.save(token, null);
|
||||
await this.pb.collection('Users').authRefresh();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Token validation failed:', error instanceof Error ? error.message : error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user record from token
|
||||
*/
|
||||
async getUserRecord(token: string): Promise<any> {
|
||||
try {
|
||||
this.pb.authStore.save(token, null);
|
||||
const record = this.pb.authStore.record || this.pb.authStore.model;
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Failed to get user record:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined backend auth manager
|
||||
*/
|
||||
export class BackendAuth {
|
||||
graphTokenManager: GraphTokenManager;
|
||||
pbValidator: PocketBaseValidator;
|
||||
|
||||
constructor(config: BackendAuthConfig, pbUrl?: string) {
|
||||
this.graphTokenManager = new GraphTokenManager(config);
|
||||
this.pbValidator = new PocketBaseValidator(pbUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to validate PocketBase token in requests
|
||||
* Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next))
|
||||
*/
|
||||
async validateTokenMiddleware(c: any, next: any): Promise<any> {
|
||||
const token = c.req.header('Authorization')?.replace('Bearer ', '') ||
|
||||
(await c.req.json().catch(() => ({})))?.pbToken;
|
||||
|
||||
if (token) {
|
||||
const isValid = await this.pbValidator.validateUserToken(token);
|
||||
if (!isValid) {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import { AuthConfig, AuthState, AuthCallbacks } from './types';
|
||||
|
||||
/**
|
||||
* PocketBase OAuth2 Frontend Module
|
||||
* Handles user authentication and token management
|
||||
*/
|
||||
export class PocketBaseAuth {
|
||||
private pb: PocketBase;
|
||||
private config: Required<AuthConfig>;
|
||||
private callbacks: AuthCallbacks;
|
||||
private state: AuthState;
|
||||
|
||||
constructor(config: AuthConfig, callbacks?: Partial<AuthCallbacks>) {
|
||||
this.config = {
|
||||
pbUrl: config.pbUrl!,
|
||||
collection: config.collection || 'Users',
|
||||
provider: config.provider || 'microsoft',
|
||||
loginContainerId: config.loginContainerId || 'loginContainer',
|
||||
userDisplayNameId: config.userDisplayNameId || 'userDisplayName',
|
||||
userEmailId: config.userEmailId || 'userEmailValue',
|
||||
loginBtnId: config.loginBtnId || 'loginBtn',
|
||||
loginErrorId: config.loginErrorId || 'loginError',
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
onAuthSuccess: callbacks?.onAuthSuccess || (() => {}),
|
||||
onAuthFailure: callbacks?.onAuthFailure || (() => {}),
|
||||
onTokenUpdate: callbacks?.onTokenUpdate || (() => {}),
|
||||
onUiUpdate: callbacks?.onUiUpdate || (() => {}),
|
||||
};
|
||||
|
||||
this.pb = new PocketBase(this.config.pbUrl);
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize auth module
|
||||
*/
|
||||
private init(): void {
|
||||
this.updateAuthUI();
|
||||
if (this.pb.authStore.isValid && this.pb.authStore.token) {
|
||||
this.ensureUserLogged();
|
||||
}
|
||||
this.setupLoginButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup login button click handler
|
||||
*/
|
||||
private setupLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId);
|
||||
if (!loginBtn) {
|
||||
console.warn(`Login button with id "${this.config.loginBtnId}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
loginBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.handleLogin();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login button click
|
||||
*/
|
||||
private async handleLogin(): Promise<void> {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement;
|
||||
|
||||
if (!loginBtn || !loginError) return;
|
||||
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Checking session...';
|
||||
loginError.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// Try to use existing token first
|
||||
const hadToken = await this.ensureUserLogged();
|
||||
if (hadToken) {
|
||||
this.resetLoginButton();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise perform OAuth
|
||||
loginBtn.textContent = 'Logging in...';
|
||||
const authData = await this.pb.collection(this.config.collection).authWithOAuth2({
|
||||
provider: this.config.provider,
|
||||
});
|
||||
|
||||
this.updateState(authData);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onAuthSuccess?.(this.state);
|
||||
|
||||
console.log('✓ Logged in with', this.config.provider);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Login failed:', message);
|
||||
loginError.textContent = `Login failed: ${message}`;
|
||||
loginError.classList.remove('hidden');
|
||||
this.callbacks.onAuthFailure?.(error);
|
||||
} finally {
|
||||
this.resetLoginButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user is logged in (check existing token)
|
||||
*/
|
||||
async ensureUserLogged(): Promise<boolean> {
|
||||
if (!this.pb.authStore.isValid || !this.pb.authStore.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refresh = await this.pb.collection(this.config.collection).authRefresh();
|
||||
this.updateState(refresh);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onTokenUpdate?.(this.state);
|
||||
console.log('✓ Token refreshed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Existing token invalid, clearing auth');
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update internal auth state
|
||||
*/
|
||||
private updateState(data: any): void {
|
||||
if (!data) {
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const record = data.record || this.pb.authStore.record || this.pb.authStore.model;
|
||||
const meta = data.meta || {};
|
||||
const model = this.pb.authStore.model;
|
||||
|
||||
this.state = {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
id: record?.id || model?.id || 'unknown',
|
||||
name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User',
|
||||
email: record?.email || model?.email || meta?.email || '(no email)',
|
||||
},
|
||||
token: this.pb.authStore.token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI based on auth state
|
||||
*/
|
||||
updateAuthUI(): void {
|
||||
const loginContainer = document.getElementById(this.config.loginContainerId);
|
||||
const userDisplayName = document.getElementById(this.config.userDisplayNameId);
|
||||
const userEmail = document.getElementById(this.config.userEmailId);
|
||||
|
||||
if (!loginContainer) return;
|
||||
|
||||
if (this.pb.authStore.isValid) {
|
||||
loginContainer.classList.add('hidden');
|
||||
if (this.state.user) {
|
||||
if (userDisplayName) userDisplayName.textContent = this.state.user.name;
|
||||
if (userEmail) userEmail.textContent = this.state.user.email;
|
||||
}
|
||||
} else {
|
||||
loginContainer.classList.remove('hidden');
|
||||
const loginError = document.getElementById(this.config.loginErrorId);
|
||||
if (loginError) loginError.classList.add('hidden');
|
||||
}
|
||||
|
||||
this.callbacks.onUiUpdate?.(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check token status (for verification/display)
|
||||
*/
|
||||
async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> {
|
||||
const pbToken = !!this.pb.authStore.token;
|
||||
return { pbToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current auth state
|
||||
*/
|
||||
getAuthState(): AuthState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance (for direct usage if needed)
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout(): void {
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
console.log('✓ Logged out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset login button to initial state
|
||||
*/
|
||||
private resetLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
if (loginBtn) {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Login with Microsoft';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick init function - fetches config from backend
|
||||
*/
|
||||
export async function initPocketBaseAuth(
|
||||
backendUrl: string,
|
||||
callbacks?: Partial<AuthCallbacks>
|
||||
): Promise<PocketBaseAuth> {
|
||||
// Fetch frontend config from backend
|
||||
const response = await fetch(`${backendUrl}/api/auth/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch auth configuration from backend');
|
||||
}
|
||||
const config = await response.json();
|
||||
|
||||
const auth = new PocketBaseAuth(config, callbacks);
|
||||
return auth;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Frontend Auth Configuration
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
pbUrl: string; // PocketBase URL (required)
|
||||
collection?: string;
|
||||
provider?: string;
|
||||
loginContainerId?: string;
|
||||
userDisplayNameId?: string;
|
||||
userEmailId?: string;
|
||||
loginBtnId?: string;
|
||||
loginErrorId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend configuration provided by backend
|
||||
*/
|
||||
export interface FrontendConfig {
|
||||
pbUrl: string;
|
||||
provider?: string;
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend Auth Configuration
|
||||
*/
|
||||
export interface BackendAuthConfig {
|
||||
clientId?: string;
|
||||
tenantId?: string;
|
||||
clientSecret?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth state object
|
||||
*/
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
} | null;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph token cache object
|
||||
*/
|
||||
export interface GraphTokenCache {
|
||||
token: string;
|
||||
expiresOn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth event callbacks
|
||||
*/
|
||||
export interface AuthCallbacks {
|
||||
onAuthSuccess?: (state: AuthState) => void;
|
||||
onAuthFailure?: (error: any) => void;
|
||||
onTokenUpdate?: (state: AuthState) => void;
|
||||
onUiUpdate?: (state: AuthState) => void;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { cors } from 'hono/cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { AuthConfigManager, BackendAuth } from '../auth-module/backend';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const app = new Hono();
|
||||
const PORT = process.env.PORT || 3005;
|
||||
|
||||
// Initialize auth
|
||||
const backendAuth = new BackendAuth({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
tenantId: process.env.TENANT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
}, process.env.PB_DB);
|
||||
|
||||
// CORS Configuration - Allow requests from frontend domains
|
||||
app.use('*', cors({
|
||||
origin: ['https://ji-test.ccllc.pro', 'http://localhost:3005', 'http://localhost:5173'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Serve bundled auth module JavaScript
|
||||
app.get('/auth-module.js', async (c) => {
|
||||
const filePath = path.join(__dirname, '../frontend/dist/auth.js');
|
||||
if (existsSync(filePath)) {
|
||||
const content = await readFile(filePath);
|
||||
c.header('Content-Type', 'application/javascript');
|
||||
return c.body(content);
|
||||
}
|
||||
return c.notFound();
|
||||
});
|
||||
|
||||
// Serve frontend static files
|
||||
app.use('/', serveStatic({ root: path.join(__dirname, '../frontend') }));
|
||||
|
||||
// API: Get frontend config
|
||||
app.get('/api/auth/config', (c) => {
|
||||
try {
|
||||
const config = AuthConfigManager.getFrontendConfig();
|
||||
return c.json(config);
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ error: 'Failed to get config', message: error instanceof Error ? error.message : 'Unknown error' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Get Graph token status
|
||||
app.get('/api/auth/graph/token', async (c) => {
|
||||
try {
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!token) {
|
||||
return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500);
|
||||
}
|
||||
|
||||
// Decode JWT to show token details (for verification)
|
||||
const parts = token.split('.');
|
||||
let payload = {};
|
||||
if (parts.length === 3) {
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
payload = {
|
||||
aud: decoded.aud,
|
||||
iss: decoded.iss,
|
||||
scp: decoded.scp,
|
||||
app_displayname: decoded.app_displayname,
|
||||
iat: new Date(decoded.iat * 1000).toISOString(),
|
||||
exp: new Date(decoded.exp * 1000).toISOString(),
|
||||
};
|
||||
} catch (e) {
|
||||
// Silent fail - just show truncated token
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
token: token.substring(0, 50) + '...',
|
||||
fullToken: token, // Include full token for testing
|
||||
expiresOn: expiresOnISO,
|
||||
isValid: backendAuth.graphTokenManager.isTokenValid(),
|
||||
tokenDetails: Object.keys(payload).length > 0 ? payload : 'Could not decode',
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to acquire token' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Validate PocketBase token
|
||||
app.post('/api/auth/validate-pb-token', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const token = body.pbToken;
|
||||
|
||||
if (!token) {
|
||||
return c.json({ valid: false, error: 'No token provided' }, 400);
|
||||
}
|
||||
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(token);
|
||||
const user = isValid ? await backendAuth.pbValidator.getUserRecord(token) : null;
|
||||
|
||||
return c.json({
|
||||
valid: isValid,
|
||||
user: user ? {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
} : null,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ valid: false, error: error instanceof Error ? error.message : 'Validation failed' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Refresh Graph token (test cache)
|
||||
app.post('/api/auth/refresh-graph', async (c) => {
|
||||
try {
|
||||
backendAuth.graphTokenManager.clearCache();
|
||||
const { token, expiresOnISO } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
return c.json({
|
||||
success: true,
|
||||
refreshed: true,
|
||||
expiresOn: expiresOnISO,
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Refresh failed' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// API: Compare PB and Graph tokens
|
||||
app.post('/api/auth/compare-tokens', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const pbToken = body.pbToken;
|
||||
|
||||
// Get Graph token
|
||||
const { token: graphToken, expiresOnISO: graphExpires } = await backendAuth.graphTokenManager.getTokenWithExpiry();
|
||||
|
||||
if (!graphToken) {
|
||||
return c.json({ success: false, error: 'Failed to acquire Graph token' }, 500);
|
||||
}
|
||||
|
||||
// Decode both tokens to compare
|
||||
const decodeJWT = (token: string) => {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
return JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const graphPayload = decodeJWT(graphToken);
|
||||
|
||||
// Validate PB token to get actual user
|
||||
let authenticatedUser = 'Unknown';
|
||||
if (pbToken) {
|
||||
try {
|
||||
const isValid = await backendAuth.pbValidator.validateUserToken(pbToken);
|
||||
if (isValid) {
|
||||
const userRecord = await backendAuth.pbValidator.getUserRecord(pbToken);
|
||||
authenticatedUser = userRecord?.email || userRecord?.id || 'Unknown';
|
||||
}
|
||||
} catch {
|
||||
authenticatedUser = 'Token invalid or expired';
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
comparison: {
|
||||
graph_token: {
|
||||
issuer: graphPayload?.iss || 'N/A',
|
||||
audience: graphPayload?.aud || 'N/A',
|
||||
app: graphPayload?.app_displayname || 'N/A',
|
||||
scopes: graphPayload?.scp || 'N/A',
|
||||
issued_at: graphPayload?.iat ? new Date(graphPayload.iat * 1000).toISOString() : 'N/A',
|
||||
expires_at: graphPayload?.exp ? new Date(graphPayload.exp * 1000).toISOString() : 'N/A',
|
||||
type: 'Microsoft Graph Token (Backend/App-only)',
|
||||
source: '@azure/msal-node',
|
||||
purpose: 'Backend API calls to Microsoft Graph',
|
||||
},
|
||||
pocketbase_token: {
|
||||
note: 'User token from PocketBase OAuth2',
|
||||
type: 'PocketBase User Token (Frontend/User)',
|
||||
source: 'PocketBase OAuth2 flow',
|
||||
purpose: 'User authentication and authorization',
|
||||
authenticated_user: authenticatedUser,
|
||||
},
|
||||
are_different: true,
|
||||
explanation: 'These are completely different tokens from different systems: Graph token is for app-to-app Microsoft API access, PocketBase token is for user authentication in the application.',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return c.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to compare tokens' },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
console.log(`Mode1 Auth Test Server running on port ${PORT}`);
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mode1-auth-test",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^5.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bun": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@azure/msal-common": ["@azure/msal-common@16.0.2", "", {}, "sha512-ZJ/UR7lyqIntURrIJCyvScwJFanM9QhJYcJCheB21jZofGKpP9QxWgvADANo7UkresHKzV+6YwoeZYP7P7HvUg=="],
|
||||
|
||||
"@azure/msal-node": ["@azure/msal-node@5.0.2", "", { "dependencies": { "@azure/msal-common": "16.0.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-3tHeJghckgpTX98TowJoXOjKGuds0L+FKfeHJtoZFl2xvwE6RF65shZJzMQ5EQZWXzh3sE1i9gE+m3aRMachjA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27rypIapNkYboOSylkf1tD9UW9Ado2I+P1NBL46Qz29KmOjTL6WuJ7mHDC5O66CYxlOkF5r93NPDAC3lFHYBXw=="],
|
||||
|
||||
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-I82xGzPkBxzBKgbl8DsA0RfMQCWTWjNmLjIEkW1ECiv3qK02kHGQ5FGUr/29L/SuvnGsULW4tBTRNZiMzL37nA=="],
|
||||
|
||||
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqtr+pTsHqusYpG2OZc6s+AmpWDB/FmBvstrK0y5zkti4OqnCuu7Ev2xNjS7uyb47NrAFF40pWqkpaio5XEd7w=="],
|
||||
|
||||
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-YaQEAYjBanoOOtpqk/c5GGcfZIyxIIkQ2m1TbHjedRmJNwxzWBhGinSARFkrRIc3F8pRIGAopXKvJ/2rjN1LzQ=="],
|
||||
|
||||
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FR+iJt17rfFgYgpxL3M67AUwujOgjw52ZJzB9vElI5jQXNjTyOKf8eH4meSk4vjlYF3h/AjKYd6pmN0OIUlVKQ=="],
|
||||
|
||||
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-egfngj0dfJ868cf30E7B+ye9KUWSebYxOG4l9YP5eWeMXCtenpenx0zdKtAn9qxJgEJym5AN6trtlk+J6x8Lig=="],
|
||||
|
||||
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-jRmnX18ak8WzqLrex3siw0PoVKyIeI5AiCv4wJLgSs7VKfOqrPycfHIWfIX2jdn7ngqbHFPzI09VBKANZ4Pckg=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-YeXcJ9K6vJAt1zSkeA21J6pTe7PgDMLTHKGI3nQBiMYnYf7Ob3K+b/ChSCznrJG7No5PCPiQPg4zTgA+BOTmSA=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-7FjVnxnRTp/AgWqSQRT/Vt9TYmvnZ+4M+d9QOKh/Lf++wIFXFGSeAgD6bV1X/yr2UPVmZDk+xdhr2XkU7l2v3w=="],
|
||||
|
||||
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-Sr1KwUcbB0SEpnSPO22tNJppku2khjFluEst+mTGhxHzAGQTQncNeJxDnt3F15n+p9Q+mlcorxehd68n1siikQ=="],
|
||||
|
||||
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-PFUa7JL4lGoyyppeS4zqfuoXXih+gSE0XxhDMrCPVEUev0yhGNd/tbWBvcdpYnUth80owENoGjc8s5Knopv9wA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.3", "", { "os": "android", "cpu": "arm" }, "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.3", "", { "os": "android", "cpu": "arm64" }, "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.3", "", { "os": "linux", "cpu": "arm" }, "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.3", "", { "os": "linux", "cpu": "arm" }, "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.3", "", { "os": "linux", "cpu": "none" }, "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.3", "", { "os": "linux", "cpu": "x64" }, "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.3", "", { "os": "none", "cpu": "arm64" }, "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.3", "", { "os": "win32", "cpu": "x64" }, "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.3", "", { "os": "win32", "cpu": "x64" }, "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bun": ["bun@1.3.6", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.6", "@oven/bun-darwin-x64": "1.3.6", "@oven/bun-darwin-x64-baseline": "1.3.6", "@oven/bun-linux-aarch64": "1.3.6", "@oven/bun-linux-aarch64-musl": "1.3.6", "@oven/bun-linux-x64": "1.3.6", "@oven/bun-linux-x64-baseline": "1.3.6", "@oven/bun-linux-x64-musl": "1.3.6", "@oven/bun-linux-x64-musl-baseline": "1.3.6", "@oven/bun-windows-x64": "1.3.6", "@oven/bun-windows-x64-baseline": "1.3.6" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-Tn98GlZVN2WM7+lg/uGn5DzUao37Yc0PUz7yzYHdeF5hd+SmHQGbCUIKE4Sspdgtxn49LunK3mDNBC2Qn6GJjw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"hono": ["hono@4.11.5", "", {}, "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.6", "", {}, "sha512-Pl7V4y3DWglYITC4cBpclmuIzePRGsb/sXk/Wyqxznwu5JsHA5IILJY81PT2XQ3OSKCakWjbxjYBqtdcghzKvA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"rollup": ["rollup@4.55.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.3", "@rollup/rollup-android-arm64": "4.55.3", "@rollup/rollup-darwin-arm64": "4.55.3", "@rollup/rollup-darwin-x64": "4.55.3", "@rollup/rollup-freebsd-arm64": "4.55.3", "@rollup/rollup-freebsd-x64": "4.55.3", "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", "@rollup/rollup-linux-arm-musleabihf": "4.55.3", "@rollup/rollup-linux-arm64-gnu": "4.55.3", "@rollup/rollup-linux-arm64-musl": "4.55.3", "@rollup/rollup-linux-loong64-gnu": "4.55.3", "@rollup/rollup-linux-loong64-musl": "4.55.3", "@rollup/rollup-linux-ppc64-gnu": "4.55.3", "@rollup/rollup-linux-ppc64-musl": "4.55.3", "@rollup/rollup-linux-riscv64-gnu": "4.55.3", "@rollup/rollup-linux-riscv64-musl": "4.55.3", "@rollup/rollup-linux-s390x-gnu": "4.55.3", "@rollup/rollup-linux-x64-gnu": "4.55.3", "@rollup/rollup-linux-x64-musl": "4.55.3", "@rollup/rollup-openbsd-x64": "4.55.3", "@rollup/rollup-openharmony-arm64": "4.55.3", "@rollup/rollup-win32-arm64-msvc": "4.55.3", "@rollup/rollup-win32-ia32-msvc": "4.55.3", "@rollup/rollup-win32-x64-gnu": "4.55.3", "@rollup/rollup-win32-x64-msvc": "4.55.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Frontend entry point - exports auth module for browser
|
||||
export { PocketBaseAuth, initPocketBaseAuth } from '../auth-module/frontend';
|
||||
export type { AuthConfig, AuthState, AuthCallbacks } from '../auth-module/types';
|
||||
@@ -0,0 +1,545 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mode1 - Auth Module Test</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 2rem;
|
||||
color: #f1f5f9;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-icon.success {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.status-icon.pending {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.status-icon.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: background 0.2s;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #64748b;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.output {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.output.success {
|
||||
border-color: #22c55e;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.output.error {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.output.warning {
|
||||
border-color: #f59e0b;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: #1e293b;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Auth Module Test Suite (Mode1)</h1>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Frontend Auth Section -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="status-icon pending" id="frontendStatus"></span>
|
||||
Frontend: PocketBase Auth
|
||||
</h2>
|
||||
|
||||
<div class="info">
|
||||
Tests OAuth2 login, token refresh, and user state management.
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="testFrontendInit()">Initialize Auth</button>
|
||||
<button onclick="testGetAuthState()">Get Auth State</button>
|
||||
</div>
|
||||
<button onclick="testFrontendLogout()">Logout</button>
|
||||
|
||||
<div class="output" id="frontendOutput"></div>
|
||||
<div class="timestamp" id="frontendTime"></div>
|
||||
</div>
|
||||
|
||||
<!-- Backend Auth Section -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="status-icon pending" id="backendStatus"></span>
|
||||
Backend: Graph Token & PB Validation
|
||||
</h2>
|
||||
|
||||
<div class="info">
|
||||
Tests Microsoft Graph token acquisition, caching, and PocketBase token validation.
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="testGraphToken()">Get Graph Token</button>
|
||||
<button onclick="testGraphRefresh()">Refresh Graph Token</button>
|
||||
</div>
|
||||
<button onclick="testCompareTokens()">Compare PB vs Graph</button>
|
||||
<button onclick="testPBValidation()">Validate PB Token</button>
|
||||
<button onclick="testCompareTokens()">Compare PB vs Graph</button>
|
||||
<div class="output" id="backendOutput"></div>
|
||||
<div class="timestamp" id="backendTime"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Info Section -->
|
||||
<div class="section">
|
||||
<h2>
|
||||
<span class="status-icon pending" id="jobInfoStatus"></span>
|
||||
Job Info (First 4 Records)
|
||||
</h2>
|
||||
|
||||
<div class="info">
|
||||
Fetch and display first 4 records from Job_Info_Prod collection.
|
||||
</div>
|
||||
|
||||
<button onclick="testJobInfo()">Fetch Job Info</button>
|
||||
|
||||
<div class="output" id="jobInfoOutput"></div>
|
||||
<div class="timestamp" id="jobInfoTime"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login container (shown when not authenticated) -->
|
||||
<div id="loginContainer" class="hidden">
|
||||
<button id="loginBtn">Login with Microsoft</button>
|
||||
<div id="loginError" class="hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- User display (shown when authenticated) -->
|
||||
<div id="userDisplayName"></div>
|
||||
<div id="userEmailValue"></div>
|
||||
|
||||
<script src="/auth-module.js"></script>
|
||||
<script>
|
||||
let pbAuth = null;
|
||||
let config = null;
|
||||
|
||||
// Utility: Log to output
|
||||
function log(elementId, message, type = 'info') {
|
||||
const output = document.getElementById(elementId);
|
||||
if (!output) return;
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const line = `[${timestamp}] ${message}`;
|
||||
|
||||
output.textContent += (output.textContent ? '\n' : '') + line;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
output.className = `output ${type}`;
|
||||
|
||||
// Update timestamp
|
||||
const timeEl = document.getElementById(elementId.replace('Output', 'Time'));
|
||||
if (timeEl) timeEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
function setStatus(elementId, status) {
|
||||
const icon = document.getElementById(elementId);
|
||||
if (!icon) return;
|
||||
icon.className = `status-icon ${status}`;
|
||||
}
|
||||
|
||||
// Test: Load config
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('./api/auth/config');
|
||||
if (!response.ok) throw new Error('Failed to load config');
|
||||
config = await response.json();
|
||||
document.getElementById('configOutput').textContent = JSON.stringify(config, null, 2);
|
||||
document.getElementById('configOutput').className = 'output success';
|
||||
} catch (error) {
|
||||
log('configOutput', `Error loading config: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Initialize Frontend Auth
|
||||
async function testFrontendInit() {
|
||||
setStatus('frontendStatus', 'pending');
|
||||
log('frontendOutput', 'Initializing PocketBaseAuth...');
|
||||
|
||||
try {
|
||||
if (typeof window.AuthModule === 'undefined' || !window.AuthModule.initPocketBaseAuth) {
|
||||
throw new Error('Auth module not loaded');
|
||||
}
|
||||
|
||||
pbAuth = await window.AuthModule.initPocketBaseAuth('.', {
|
||||
onAuthSuccess: (state) => {
|
||||
log('frontendOutput', `✓ Auth success: ${state.user?.email}`, 'success');
|
||||
setStatus('frontendStatus', 'success');
|
||||
},
|
||||
onAuthFailure: (error) => {
|
||||
log('frontendOutput', `✗ Auth failed: ${error.message}`, 'error');
|
||||
setStatus('frontendStatus', 'error');
|
||||
},
|
||||
onTokenUpdate: (state) => {
|
||||
log('frontendOutput', `✓ Token refreshed for ${state.user?.email}`, 'success');
|
||||
},
|
||||
onUiUpdate: (state) => {
|
||||
log('frontendOutput', `✓ UI updated: ${state.isAuthenticated ? 'Authenticated' : 'Not authenticated'}`);
|
||||
},
|
||||
});
|
||||
|
||||
log('frontendOutput', '✓ Frontend auth initialized', 'success');
|
||||
setStatus('frontendStatus', 'success');
|
||||
} catch (error) {
|
||||
log('frontendOutput', `✗ Initialization failed: ${error.message}`, 'error');
|
||||
setStatus('frontendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Get Auth State
|
||||
function testGetAuthState() {
|
||||
if (!pbAuth) {
|
||||
log('frontendOutput', '✗ Auth not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const state = pbAuth.getAuthState();
|
||||
log('frontendOutput', `Auth State: ${JSON.stringify(state, null, 2)}`, 'success');
|
||||
}
|
||||
|
||||
// Test: Frontend Logout
|
||||
function testFrontendLogout() {
|
||||
if (!pbAuth) {
|
||||
log('frontendOutput', '✗ Auth not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
pbAuth.logout();
|
||||
log('frontendOutput', '✓ Logged out', 'success');
|
||||
setStatus('frontendStatus', 'pending');
|
||||
}
|
||||
|
||||
// Test: Get Graph Token
|
||||
async function testGraphToken() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Requesting Graph token from backend...');
|
||||
|
||||
try {
|
||||
const response = await fetch('./api/auth/graph/token');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
let output = `✓ Token acquired\n`;
|
||||
output += `Expires: ${data.expiresOn}\n`;
|
||||
output += `Valid in cache: ${data.isValid}\n\n`;
|
||||
|
||||
if (data.tokenDetails && typeof data.tokenDetails === 'object') {
|
||||
output += `Token Details:\n`;
|
||||
output += ` Audience: ${data.tokenDetails.aud}\n`;
|
||||
output += ` Issuer: ${data.tokenDetails.iss}\n`;
|
||||
output += ` Scopes: ${data.tokenDetails.scp}\n`;
|
||||
output += ` App: ${data.tokenDetails.app_displayname}\n`;
|
||||
output += ` Issued: ${data.tokenDetails.iat}\n`;
|
||||
output += ` Expires: ${data.tokenDetails.exp}\n\n`;
|
||||
}
|
||||
|
||||
output += `Full Token (first 100 chars):\n${data.fullToken.substring(0, 100)}...`;
|
||||
|
||||
log('backendOutput', output, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Request failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Refresh Graph Token
|
||||
async function testGraphRefresh() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Refreshing Graph token...');
|
||||
|
||||
try {
|
||||
const response = await fetch('./api/auth/refresh-graph', { method: 'POST' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
log('backendOutput', `✓ Token refreshed\nExpires: ${data.expiresOn}`, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Refresh failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Compare tokens
|
||||
async function testCompareTokens() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Comparing PocketBase token vs Graph token...');
|
||||
|
||||
if (!pbAuth) {
|
||||
log('backendOutput', '✗ Auth not initialized', 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = pbAuth.getAuthState();
|
||||
const pbToken = state.token || null;
|
||||
|
||||
const response = await fetch('./api/auth/compare-tokens', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.comparison) {
|
||||
let output = `✓ Token Comparison:\n\n`;
|
||||
|
||||
output += `GRAPH TOKEN (Backend/App-only):\n`;
|
||||
output += ` Type: ${data.comparison.graph_token.type}\n`;
|
||||
output += ` Source: ${data.comparison.graph_token.source}\n`;
|
||||
output += ` Issuer: ${data.comparison.graph_token.issuer}\n`;
|
||||
output += ` Audience: ${data.comparison.graph_token.audience}\n`;
|
||||
output += ` App: ${data.comparison.graph_token.app}\n`;
|
||||
output += ` Scopes: ${data.comparison.graph_token.scopes}\n`;
|
||||
output += ` Purpose: ${data.comparison.graph_token.purpose}\n\n`;
|
||||
|
||||
output += `POCKETBASE TOKEN (Frontend/User):\n`;
|
||||
output += ` Type: ${data.comparison.pocketbase_token.type}\n`;
|
||||
output += ` Source: ${data.comparison.pocketbase_token.source}\n`;
|
||||
output += ` User: ${data.comparison.pocketbase_token.authenticated_user}\n`;
|
||||
output += ` Purpose: ${data.comparison.pocketbase_token.purpose}\n\n`;
|
||||
|
||||
output += `COMPARISON:\n`;
|
||||
output += ` Are Different: YES ✓\n`;
|
||||
output += ` ${data.comparison.explanation}\n`;
|
||||
|
||||
log('backendOutput', output, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Comparison failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Validate PocketBase Token
|
||||
async function testPBValidation() {
|
||||
setStatus('backendStatus', 'pending');
|
||||
log('backendOutput', 'Validating PocketBase token...');
|
||||
|
||||
if (!pbAuth) {
|
||||
log('backendOutput', '✗ Auth not initialized', 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = pbAuth.getAuthState();
|
||||
|
||||
if (!state.token) {
|
||||
log('backendOutput', '✗ No token available - user not logged in', 'warning');
|
||||
setStatus('backendStatus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
log('backendOutput', `Sending token for user: ${state.user?.email}...`);
|
||||
|
||||
const response = await fetch('./api/auth/validate-pb-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken: state.token }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.valid) {
|
||||
log('backendOutput', `✓ Token valid\nUser: ${data.user?.email}\nID: ${data.user?.id}`, 'success');
|
||||
setStatus('backendStatus', 'success');
|
||||
} else {
|
||||
log('backendOutput', `✗ Token invalid: ${data.error}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('backendOutput', `✗ Validation failed: ${error.message}`, 'error');
|
||||
setStatus('backendStatus', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Fetch Job Info
|
||||
async function testJobInfo() {
|
||||
setStatus('jobInfoStatus', 'pending');
|
||||
log('jobInfoOutput', 'Fetching first 4 records from Job_Info_Prod...');
|
||||
|
||||
if (!pbAuth) {
|
||||
log('jobInfoOutput', '✗ Auth not initialized - cannot fetch records', 'error');
|
||||
setStatus('jobInfoStatus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pb = pbAuth.getPocketBase();
|
||||
const records = await pb.collection('Job_Info_Prod').getList(1, 4);
|
||||
|
||||
if (!records.items || records.items.length === 0) {
|
||||
log('jobInfoOutput', 'No records found', 'warning');
|
||||
setStatus('jobInfoStatus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
let output = `✓ Found ${records.items.length} records:\n\n`;
|
||||
|
||||
records.items.forEach((record, index) => {
|
||||
output += `[${index + 1}] ID: ${record.id}\n`;
|
||||
output += ` Title: ${record.title || record.name || 'N/A'}\n`;
|
||||
output += ` Status: ${record.status || 'N/A'}\n`;
|
||||
output += ` Created: ${new Date(record.created).toLocaleDateString()}\n\n`;
|
||||
});
|
||||
|
||||
log('jobInfoOutput', output, 'success');
|
||||
setStatus('jobInfoStatus', 'success');
|
||||
} catch (error) {
|
||||
log('jobInfoOutput', `✗ Failed to fetch records: ${error.message}`, 'error');
|
||||
setStatus('jobInfoStatus', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "job-info-pb",
|
||||
"version": "1.0.0-beta3",
|
||||
"name": "mode1-auth-test",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --watch backend/server.ts",
|
||||
"start": "bun run backend/server.ts",
|
||||
"build:css": "tailwindcss -i input.css -o frontend/output.css --watch"
|
||||
"dev": "bun --watch backend/server.ts",
|
||||
"build": "vite build",
|
||||
"prebuild": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^5.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18"
|
||||
"typescript": "^5",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0"
|
||||
"bun": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'frontend/dist',
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: 'frontend/auth.ts',
|
||||
name: 'AuthModule',
|
||||
fileName: () => 'auth.js',
|
||||
formats: ['umd']
|
||||
}
|
||||
},
|
||||
server: {
|
||||
middlewareMode: true
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
@@ -0,0 +1,313 @@
|
||||
# Mode1Svelte - SvelteKit Auth Testing Suite
|
||||
|
||||
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.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: SvelteKit 2.x with TypeScript
|
||||
- **Backend**: Node.js Adapter (production-ready)
|
||||
- **Styling**: Tailwind CSS 4
|
||||
- **Authentication**:
|
||||
- Azure MSAL Node (@azure/msal-node) - Microsoft Graph token management
|
||||
- PocketBase - User OAuth2 and token validation
|
||||
- **Build Tool**: Vite (integrated with SvelteKit)
|
||||
- **Runtime**: Node.js 18+
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Frontend Authentication (PocketBase)
|
||||
- OAuth2 login initialization with Microsoft
|
||||
- Auth state management and retrieval
|
||||
- Token refresh and validation
|
||||
- Session persistence
|
||||
- Automatic logout on invalid tokens
|
||||
|
||||
### ✅ Backend Token Management (Microsoft Graph)
|
||||
- App-only token acquisition via MSAL
|
||||
- Automatic token caching with 60-second expiration buffer
|
||||
- Token refresh on demand
|
||||
- JWT payload inspection
|
||||
|
||||
### ✅ Token Validation & Comparison
|
||||
- PocketBase user token validation
|
||||
- User record retrieval from tokens
|
||||
- Side-by-side token comparison (Graph vs PocketBase)
|
||||
- Token payload analysis
|
||||
|
||||
### ✅ Interactive Testing Dashboard
|
||||
- Real-time authentication status
|
||||
- Visual token testing interface
|
||||
- API endpoint testing
|
||||
- Response inspection
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Mode1Svelte/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ └── auth/
|
||||
│ │ ├── 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
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd Mode1Svelte
|
||||
npm install
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Ensure your `.env` file or environment variables are set:
|
||||
|
||||
```bash
|
||||
export CLIENT_ID="<Azure Client ID>"
|
||||
export TENANT_ID="<Azure Tenant ID>"
|
||||
export CLIENT_SECRET="<Azure Client Secret>"
|
||||
export PB_URL="<PocketBase URL>"
|
||||
export PB_DB="<PocketBase Database URL>"
|
||||
export PORT=5173
|
||||
```
|
||||
|
||||
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
|
||||
npm run dev
|
||||
```
|
||||
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
|
||||
|
||||
### GET `/api/auth/config`
|
||||
Returns frontend-safe configuration for PocketBase OAuth setup.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"pbUrl": "https://pocketbase.example.com",
|
||||
"provider": "microsoft",
|
||||
"collection": "Users"
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/api/auth/graph/token`
|
||||
Fetches a Microsoft Graph token for backend API access.
|
||||
|
||||
**Response:**
|
||||
```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": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/validate-pb-token`
|
||||
Validates a PocketBase user token and retrieves user information.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"pbToken": "user_token_string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"user": {
|
||||
"id": "user_id",
|
||||
"email": "user@example.com",
|
||||
"name": "User Name"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/refresh-graph`
|
||||
Clears the cached Graph token and acquires a new one.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"refreshed": true,
|
||||
"expiresOn": "2026-01-23T02:15:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/compare-tokens`
|
||||
Compares Graph token (backend) and PocketBase token (user) to demonstrate their differences.
|
||||
|
||||
**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.
|
||||
Generated
+2823
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "mode1svelte",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3005",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "PORT=3005 node build/index.js",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.2",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.45.6",
|
||||
"svelte-check": "^4.3.4",
|
||||
"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 |
@@ -0,0 +1,204 @@
|
||||
import { config } from 'dotenv';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import PocketBase from 'pocketbase';
|
||||
import type { GraphTokenCache, BackendAuthConfig } from './types';
|
||||
|
||||
// Load environment variables from shared secrets directory
|
||||
config({ path: '/home/admin/secrets/.env' });
|
||||
|
||||
/**
|
||||
* Configuration Manager
|
||||
* Exposes frontend-safe configuration loaded from environment
|
||||
*/
|
||||
export class AuthConfigManager {
|
||||
/**
|
||||
* Get frontend configuration (safe to expose to browser)
|
||||
*/
|
||||
static getFrontendConfig() {
|
||||
return {
|
||||
pbUrl: process.env.PB_URL!,
|
||||
provider: 'microsoft',
|
||||
collection: 'Users',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all backend secrets (never expose to frontend)
|
||||
*/
|
||||
static getBackendConfig() {
|
||||
return {
|
||||
clientId: process.env.CLIENT_ID!,
|
||||
tenantId: process.env.TENANT_ID!,
|
||||
clientSecret: process.env.CLIENT_SECRET!,
|
||||
pbDb: process.env.PB_DB!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Graph Token Management (Backend)
|
||||
* Handles token acquisition and caching for backend Graph API calls
|
||||
*/
|
||||
export class GraphTokenManager {
|
||||
private cca: ConfidentialClientApplication;
|
||||
private cache: GraphTokenCache | null = null;
|
||||
private config: Required<BackendAuthConfig>;
|
||||
|
||||
constructor(config: BackendAuthConfig) {
|
||||
this.config = {
|
||||
clientId: config.clientId || process.env.CLIENT_ID || '',
|
||||
tenantId: config.tenantId || process.env.TENANT_ID || '',
|
||||
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
|
||||
};
|
||||
|
||||
this.cca = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId: this.config.clientId,
|
||||
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
|
||||
clientSecret: this.config.clientSecret,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Graph token (from cache or acquire new)
|
||||
*/
|
||||
async getToken(): Promise<string> {
|
||||
const now = Date.now();
|
||||
|
||||
// Check cache validity (with 60s buffer for expiration)
|
||||
if (this.cache && this.cache.expiresOn - 60000 > now) {
|
||||
return this.cache.token;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.cca.acquireTokenByClientCredential({
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
});
|
||||
|
||||
if (!result?.accessToken) {
|
||||
throw new Error('Failed to acquire Graph token');
|
||||
}
|
||||
|
||||
const expiresOn = result.expiresOn
|
||||
? new Date(result.expiresOn).getTime()
|
||||
: now + 55 * 60 * 1000; // Default 55 minutes
|
||||
|
||||
this.cache = {
|
||||
token: result.accessToken,
|
||||
expiresOn,
|
||||
};
|
||||
|
||||
return result.accessToken;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new Error(`Failed to acquire Graph token: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token with expiration info
|
||||
*/
|
||||
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
|
||||
const token = await this.getToken();
|
||||
const expiresOn = this.cache?.expiresOn || Date.now();
|
||||
return {
|
||||
token,
|
||||
expiresOnISO: new Date(expiresOn).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is cached and valid
|
||||
*/
|
||||
isTokenValid(): boolean {
|
||||
if (!this.cache) return false;
|
||||
const now = Date.now();
|
||||
return this.cache.expiresOn - 60000 > now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache (force refresh on next call)
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PocketBase Token Validation (Backend)
|
||||
* Validates and uses user PocketBase tokens
|
||||
*/
|
||||
export class PocketBaseValidator {
|
||||
private pb: PocketBase;
|
||||
|
||||
constructor(pbUrl?: string) {
|
||||
this.pb = new PocketBase(pbUrl || process.env.PB_DB!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user token and validate it
|
||||
*/
|
||||
async validateUserToken(token: string): Promise<boolean> {
|
||||
try {
|
||||
this.pb.authStore.save(token, null);
|
||||
await this.pb.collection('Users').authRefresh();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Token validation failed:', error instanceof Error ? error.message : error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user record from token
|
||||
*/
|
||||
async getUserRecord(token: string): Promise<any> {
|
||||
try {
|
||||
this.pb.authStore.save(token, null);
|
||||
const record = this.pb.authStore.record || this.pb.authStore.model;
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Failed to get user record:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined backend auth manager
|
||||
*/
|
||||
export class BackendAuth {
|
||||
graphTokenManager: GraphTokenManager;
|
||||
pbValidator: PocketBaseValidator;
|
||||
|
||||
constructor(config: BackendAuthConfig, pbUrl?: string) {
|
||||
this.graphTokenManager = new GraphTokenManager(config);
|
||||
this.pbValidator = new PocketBaseValidator(pbUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to validate PocketBase token in requests
|
||||
* Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next))
|
||||
*/
|
||||
async validateTokenMiddleware(c: any, next: any): Promise<any> {
|
||||
const token = c.req.header('Authorization')?.replace('Bearer ', '') ||
|
||||
(await c.req.json().catch(() => ({})))?.pbToken;
|
||||
|
||||
if (token) {
|
||||
const isValid = await this.pbValidator.validateUserToken(token);
|
||||
if (!isValid) {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import type { AuthConfig, AuthState, AuthCallbacks } from './types';
|
||||
|
||||
/**
|
||||
* PocketBase OAuth2 Frontend Module
|
||||
* Handles user authentication and token management
|
||||
*/
|
||||
export class PocketBaseAuth {
|
||||
private pb: PocketBase;
|
||||
private config: Required<AuthConfig>;
|
||||
private callbacks: AuthCallbacks;
|
||||
private state: AuthState;
|
||||
|
||||
constructor(config: AuthConfig, callbacks?: Partial<AuthCallbacks>) {
|
||||
this.config = {
|
||||
pbUrl: config.pbUrl!,
|
||||
collection: config.collection || 'Users',
|
||||
provider: config.provider || 'microsoft',
|
||||
loginContainerId: config.loginContainerId || 'loginContainer',
|
||||
userDisplayNameId: config.userDisplayNameId || 'userDisplayName',
|
||||
userEmailId: config.userEmailId || 'userEmailValue',
|
||||
loginBtnId: config.loginBtnId || 'loginBtn',
|
||||
loginErrorId: config.loginErrorId || 'loginError',
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
onAuthSuccess: callbacks?.onAuthSuccess || (() => {}),
|
||||
onAuthFailure: callbacks?.onAuthFailure || (() => {}),
|
||||
onTokenUpdate: callbacks?.onTokenUpdate || (() => {}),
|
||||
onUiUpdate: callbacks?.onUiUpdate || (() => {}),
|
||||
};
|
||||
|
||||
this.pb = new PocketBase(this.config.pbUrl);
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize auth module
|
||||
*/
|
||||
private init(): void {
|
||||
this.updateAuthUI();
|
||||
if (this.pb.authStore.isValid && this.pb.authStore.token) {
|
||||
this.ensureUserLogged();
|
||||
}
|
||||
this.setupLoginButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup login button click handler
|
||||
*/
|
||||
private setupLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId);
|
||||
if (!loginBtn) {
|
||||
console.warn(`Login button with id "${this.config.loginBtnId}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
loginBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.handleLogin();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login button click
|
||||
*/
|
||||
private async handleLogin(): Promise<void> {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement;
|
||||
|
||||
if (!loginBtn || !loginError) return;
|
||||
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Checking session...';
|
||||
loginError.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// Try to use existing token first
|
||||
const hadToken = await this.ensureUserLogged();
|
||||
if (hadToken) {
|
||||
this.resetLoginButton();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise perform OAuth
|
||||
loginBtn.textContent = 'Logging in...';
|
||||
const authData = await this.pb.collection(this.config.collection).authWithOAuth2({
|
||||
provider: this.config.provider,
|
||||
});
|
||||
|
||||
this.updateState(authData);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onAuthSuccess?.(this.state);
|
||||
|
||||
console.log('✓ Logged in with', this.config.provider);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Login failed:', message);
|
||||
loginError.textContent = `Login failed: ${message}`;
|
||||
loginError.classList.remove('hidden');
|
||||
this.callbacks.onAuthFailure?.(error);
|
||||
} finally {
|
||||
this.resetLoginButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user is logged in (check existing token)
|
||||
*/
|
||||
async ensureUserLogged(): Promise<boolean> {
|
||||
if (!this.pb.authStore.isValid || !this.pb.authStore.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refresh = await this.pb.collection(this.config.collection).authRefresh();
|
||||
this.updateState(refresh);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onTokenUpdate?.(this.state);
|
||||
console.log('✓ Token refreshed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Existing token invalid, clearing auth');
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update internal auth state
|
||||
*/
|
||||
private updateState(data: any): void {
|
||||
if (!data) {
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const record = data.record || this.pb.authStore.record || this.pb.authStore.model;
|
||||
const meta = data.meta || {};
|
||||
const model = this.pb.authStore.model;
|
||||
|
||||
this.state = {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
id: record?.id || model?.id || 'unknown',
|
||||
name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User',
|
||||
email: record?.email || model?.email || meta?.email || '(no email)',
|
||||
},
|
||||
token: this.pb.authStore.token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI based on auth state
|
||||
*/
|
||||
updateAuthUI(): void {
|
||||
const loginContainer = document.getElementById(this.config.loginContainerId);
|
||||
const userDisplayName = document.getElementById(this.config.userDisplayNameId);
|
||||
const userEmail = document.getElementById(this.config.userEmailId);
|
||||
|
||||
if (!loginContainer) return;
|
||||
|
||||
if (this.pb.authStore.isValid) {
|
||||
loginContainer.classList.add('hidden');
|
||||
if (this.state.user) {
|
||||
if (userDisplayName) userDisplayName.textContent = this.state.user.name;
|
||||
if (userEmail) userEmail.textContent = this.state.user.email;
|
||||
}
|
||||
} else {
|
||||
loginContainer.classList.remove('hidden');
|
||||
const loginError = document.getElementById(this.config.loginErrorId);
|
||||
if (loginError) loginError.classList.add('hidden');
|
||||
}
|
||||
|
||||
this.callbacks.onUiUpdate?.(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check token status (for verification/display)
|
||||
*/
|
||||
async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> {
|
||||
const pbToken = !!this.pb.authStore.token;
|
||||
return { pbToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current auth state
|
||||
*/
|
||||
getAuthState(): AuthState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance (for direct usage if needed)
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout(): void {
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
console.log('✓ Logged out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset login button to initial state
|
||||
*/
|
||||
private resetLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
if (loginBtn) {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Login with Microsoft';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick init function - fetches config from backend
|
||||
*/
|
||||
export async function initPocketBaseAuth(
|
||||
backendUrl: string,
|
||||
callbacks?: Partial<AuthCallbacks>
|
||||
): Promise<PocketBaseAuth> {
|
||||
// Fetch frontend config from backend
|
||||
const response = await fetch(`${backendUrl}/api/auth/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch auth configuration from backend');
|
||||
}
|
||||
const config = await response.json();
|
||||
|
||||
const auth = new PocketBaseAuth(config, callbacks);
|
||||
return auth;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Frontend Auth Configuration
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
pbUrl: string; // PocketBase URL (required)
|
||||
collection?: string;
|
||||
provider?: string;
|
||||
loginContainerId?: string;
|
||||
userDisplayNameId?: string;
|
||||
userEmailId?: string;
|
||||
loginBtnId?: string;
|
||||
loginErrorId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend configuration provided by backend
|
||||
*/
|
||||
export interface FrontendConfig {
|
||||
pbUrl: string;
|
||||
provider?: string;
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend Auth Configuration
|
||||
*/
|
||||
export interface BackendAuthConfig {
|
||||
clientId?: string;
|
||||
tenantId?: string;
|
||||
clientSecret?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth state object
|
||||
*/
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
} | null;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph token cache object
|
||||
*/
|
||||
export interface GraphTokenCache {
|
||||
token: string;
|
||||
expiresOn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth event callbacks
|
||||
*/
|
||||
export interface AuthCallbacks {
|
||||
onAuthSuccess?: (state: AuthState) => void;
|
||||
onAuthFailure?: (error: any) => void;
|
||||
onTokenUpdate?: (state: AuthState) => void;
|
||||
onUiUpdate?: (state: AuthState) => void;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// 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;
|
||||
@@ -1,10 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./frontend/**/*.{html,js}',
|
||||
],
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
build: {
|
||||
// Ensure asset paths are relative, not absolute
|
||||
// This fixes proxy routing issues
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'assets/[name]-[hash][extname]'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,513 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import fs from 'fs';
|
||||
// @ts-ignore Bun project may not have dotenv types configured
|
||||
import { config } from 'dotenv';
|
||||
import { getCache, setCache, isRedisConnected } from './redis-cache';
|
||||
|
||||
config();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Minimal log helpers
|
||||
const logLine = (path: string, line: string) => {
|
||||
try {
|
||||
fs.appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
|
||||
} catch (err) {
|
||||
console.error('Failed to write log:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Version endpoint sourced from package.json
|
||||
app.get('/version', (c) => {
|
||||
try {
|
||||
const pkgRaw = fs.readFileSync('./package.json', 'utf-8');
|
||||
const pkg = JSON.parse(pkgRaw);
|
||||
return c.json({ version: pkg.version || '0.0.0' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `version endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ version: '0.0.0' }, 200);
|
||||
}
|
||||
});
|
||||
|
||||
// Jobs endpoint with Redis caching
|
||||
app.get('/api/jobs', async (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = parseInt(c.req.query('perPage') || '50');
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
const cacheKey = `jobs:page:${page}:perPage:${perPage}:sort:${sort}`;
|
||||
const ttl = 300; // 5 minutes cache
|
||||
|
||||
try {
|
||||
// Try to get from cache first
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey}`);
|
||||
return c.json(cached);
|
||||
}
|
||||
logLine('logs/server.log', `Cache MISS for ${cacheKey}`);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=${sort}`;
|
||||
const authHeader = c.req.header('Authorization') || '';
|
||||
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache the result
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, data, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} for ${ttl}s`);
|
||||
}
|
||||
|
||||
return c.json(data);
|
||||
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Fast endpoint: returns all jobs in a single cached request
|
||||
app.get('/api/jobs-all', async (c) => {
|
||||
const cacheKey = 'jobs:all';
|
||||
const ttl = 1800; // 30 minutes cache
|
||||
try {
|
||||
// Try cache first for instant response
|
||||
if (isRedisConnected()) {
|
||||
const cached = await getCache(cacheKey);
|
||||
if (cached) {
|
||||
logLine('logs/server.log', `Cache HIT for ${cacheKey} (${cached.items?.length || 0} jobs)`);
|
||||
// Return cached data immediately
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await refreshJobsCache(cacheKey, ttl, c.req.header('Authorization') || '');
|
||||
} catch (err) {}
|
||||
});
|
||||
// Return all cached jobs in a single response
|
||||
return c.json({ ...cached, partial: false });
|
||||
} else {
|
||||
// No cache: return empty array instantly, trigger background fetch
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
|
||||
if (isRedisConnected()) {
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Cached ${cacheKey} with ${result.items.length} jobs for ${ttl}s`);
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all background fetch error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
});
|
||||
return c.json({ items: [], partial: true });
|
||||
}
|
||||
}
|
||||
// Redis not connected: fallback to direct fetch (blocking)
|
||||
const result = await fetchAllJobsFromPocketBase(c.req.header('Authorization') || '');
|
||||
return c.json(result);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `jobs-all endpoint error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.json({ error: 'Failed to fetch jobs' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to fetch all jobs from PocketBase
|
||||
async function fetchAllJobsFromPocketBase(authHeader: string): Promise<any> {
|
||||
const allItems: any[] = [];
|
||||
let page = 1;
|
||||
const perPage = 500;
|
||||
|
||||
while (true) {
|
||||
const pbUrl = `https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records?page=${page}&perPage=${perPage}&sort=-Job_Number`;
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PocketBase returned ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
|
||||
if (items.length === 0) break;
|
||||
allItems.push(...items);
|
||||
|
||||
if (allItems.length >= (data.totalItems || 0)) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
return {
|
||||
page: 1,
|
||||
perPage: allItems.length,
|
||||
totalItems: allItems.length,
|
||||
totalPages: 1,
|
||||
items: allItems
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to refresh jobs cache in background
|
||||
async function refreshJobsCache(cacheKey: string, ttl: number, authHeader: string): Promise<void> {
|
||||
try {
|
||||
const result = await fetchAllJobsFromPocketBase(authHeader);
|
||||
await setCache(cacheKey, result, ttl);
|
||||
logLine('logs/server.log', `Background refresh: cached ${cacheKey} with ${result.items.length} jobs`);
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Background refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Microsoft Graph helpers ---
|
||||
const getGraphToken = (c?: any) => {
|
||||
const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
|
||||
console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET');
|
||||
console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE');
|
||||
return token;
|
||||
};
|
||||
|
||||
const b64Url = (input: string) =>
|
||||
Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
type DriveItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
webUrl?: string;
|
||||
size?: number;
|
||||
lastModifiedDateTime?: string;
|
||||
file?: { mimeType?: string };
|
||||
folder?: { childCount?: number };
|
||||
parentReference?: { path?: string; driveId?: string };
|
||||
};
|
||||
|
||||
const fetchJson = async (url: string, token: string) => {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err: any = new Error(`Graph ${res.status}: ${text}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const resolveShareLink = async (link: string, token: string) => {
|
||||
// Decode the link in case it comes pre-encoded from the database
|
||||
const decodedLink = decodeURIComponent(link);
|
||||
console.log('[resolveShareLink] Decoded link:', decodedLink);
|
||||
|
||||
// Check if this is a short sharing link (/:f:/ or /:b:/) or a direct document library URL
|
||||
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||
// Short sharing link - use shares API
|
||||
const encoded = `u!${b64Url(decodedLink)}`;
|
||||
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
} else {
|
||||
// Direct document library URL - parse it and use drive API
|
||||
// Example: https://czflex.sharepoint.com/sites/Team/Shared Documents/Forms/AllItems.aspx?RootFolder=/sites/Team/Shared Documents/General/...
|
||||
const url = new URL(decodedLink);
|
||||
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||
|
||||
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||
const sitePath = url.pathname.split('/Shared')[0]; // e.g., /sites/Team
|
||||
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||
if (!rootFolderParam) throw new Error('RootFolder parameter missing');
|
||||
|
||||
// Extract the folder path relative to the document library
|
||||
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||
// We need: General/Operations [Server]/...
|
||||
const folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||
if (!folderPath) throw new Error('Could not parse folder path');
|
||||
|
||||
console.log('[resolveShareLink] Hostname:', hostname);
|
||||
console.log('[resolveShareLink] Site path:', sitePath);
|
||||
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||
|
||||
// Get the site ID first using hostname:sitePath format
|
||||
const siteData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||
const siteId = siteData.id;
|
||||
|
||||
// Get the default document library drive
|
||||
const driveData = await fetchJson(`https://graph.microsoft.com/v1.0/sites/${siteId}/drive`, token);
|
||||
const driveId = driveData.id;
|
||||
|
||||
// Get the folder item by path
|
||||
const itemData = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/${folderPath}`,
|
||||
token
|
||||
);
|
||||
|
||||
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||
return { driveId, itemId: itemData.id };
|
||||
}
|
||||
};
|
||||
|
||||
const listChildrenRecursive = async (
|
||||
driveId: string,
|
||||
itemId: string,
|
||||
depth = 0,
|
||||
maxDepth = 5,
|
||||
token: string,
|
||||
): Promise<DriveItem[]> => {
|
||||
const out: DriveItem[] = [];
|
||||
const data = await fetchJson(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/children`, token);
|
||||
for (const child of data.value || []) {
|
||||
out.push(child as DriveItem);
|
||||
if (child.folder && depth < maxDepth) {
|
||||
const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
|
||||
out.push(...nested);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const searchWithinFolder = async (driveId: string, itemId: string, query: string, token: string): Promise<DriveItem[]> => {
|
||||
const data = await fetchJson(
|
||||
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/search(q='${encodeURIComponent(query)}')`,
|
||||
token,
|
||||
);
|
||||
return (data.value || []) as DriveItem[];
|
||||
};
|
||||
|
||||
const filterByCategory = (items: DriveItem[], category?: string, pdfOnly: boolean = true) => {
|
||||
// First filter: show PDF files and Word documents (which will be converted to PDF)
|
||||
let filtered = items;
|
||||
if (pdfOnly) {
|
||||
filtered = items.filter((i) => {
|
||||
if (i.folder) return false;
|
||||
const name = i.name.toLowerCase();
|
||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||
// Include PDFs, Word documents, and images
|
||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || mimeType.includes('image');
|
||||
});
|
||||
}
|
||||
|
||||
// Second filter: category filtering (if specified)
|
||||
if (!category) return filtered;
|
||||
const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
|
||||
const lc = (s: string) => (s || '').toLowerCase();
|
||||
const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
|
||||
const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
|
||||
return filtered.filter((i) => {
|
||||
const n = lc(i.name);
|
||||
if (category === 'contracts') return nameHas(n, contractWords);
|
||||
if (category === 'plans') return nameHas(n, planWords);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// List/search job files via Graph using a shared folder link
|
||||
app.get('/api/job-files', async (c) => {
|
||||
try {
|
||||
console.log('[job-files] Request received');
|
||||
const token = getGraphToken(c);
|
||||
if (!token) {
|
||||
console.log('[job-files] No token found');
|
||||
return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
}
|
||||
console.log('[job-files] Token found:', token.substring(0, 20) + '...');
|
||||
const link = c.req.query('link');
|
||||
const q = c.req.query('q') || '';
|
||||
const category = c.req.query('category') || '';
|
||||
if (!link) return c.json({ error: 'link required' }, 400);
|
||||
console.log('[job-files] Link:', link);
|
||||
|
||||
const { driveId, itemId } = await resolveShareLink(link, token);
|
||||
|
||||
let items: DriveItem[] = [];
|
||||
if (q) {
|
||||
items = await searchWithinFolder(driveId, itemId, q, token);
|
||||
} else {
|
||||
// Walk subfolders up to depth 5
|
||||
items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
|
||||
}
|
||||
|
||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||
const pdfOnly = c.req.query('pdfOnly') !== 'false';
|
||||
const filtered = filterByCategory(items, category, pdfOnly);
|
||||
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||
|
||||
const mapped = filtered.map((i) => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
url: i.webUrl,
|
||||
driveId: i.parentReference?.driveId || driveId,
|
||||
size: i.size,
|
||||
modified: i.lastModifiedDateTime,
|
||||
contentType: i.file?.mimeType,
|
||||
isFolder: Boolean(i.folder),
|
||||
path: i.parentReference?.path || '',
|
||||
}));
|
||||
|
||||
return c.json({ items: mapped, total: mapped.length, source: q ? 'search' : 'walk', driveId });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
console.error('[job-files] ERROR:', message);
|
||||
logLine('logs/error.log', `/api/job-files error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream file content via Graph to avoid CSP issues for inline preview
|
||||
app.get('/api/job-file-content', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-content failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `Graph ${res.status}: ${text}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {};
|
||||
const ct = res.headers.get('content-type');
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (ct) headers['Content-Type'] = ct;
|
||||
if (cd) headers['Content-Disposition'] = cd;
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-content error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Word documents to PDF for display
|
||||
app.get('/api/job-file-pdf', async (c) => {
|
||||
try {
|
||||
const token = getGraphToken(c);
|
||||
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
|
||||
const driveId = c.req.query('driveId');
|
||||
const itemId = c.req.query('itemId');
|
||||
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
|
||||
|
||||
// Use Graph API to convert to PDF format
|
||||
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
logLine('logs/error.log', `/api/job-file-pdf conversion failed: ${res.status} - ${text}`);
|
||||
return c.json({ error: `PDF conversion failed: ${res.status}` }, 502);
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/pdf',
|
||||
};
|
||||
const cd = res.headers.get('content-disposition');
|
||||
if (cd) {
|
||||
// Replace .docx/.doc extension with .pdf in filename
|
||||
headers['Content-Disposition'] = cd.replace(/\.(docx?)/gi, '.pdf');
|
||||
}
|
||||
logLine('logs/server.log', `Successfully converted document to PDF: driveId=${driveId}, itemId=${itemId}`);
|
||||
return new Response(res.body, { status: 200, headers });
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
logLine('logs/error.log', `/api/job-file-pdf error: ${message}`);
|
||||
return c.json({ error: message }, status);
|
||||
}
|
||||
});
|
||||
|
||||
// Mutation logging endpoint
|
||||
app.post('/log-change', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { action = 'unknown', user = 'unknown', target = '', detail = '' } = body || {};
|
||||
const payload = JSON.stringify({ action, user, target, detail });
|
||||
logLine('logs/changes.log', payload);
|
||||
return c.json({ status: 'ok' });
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `log-change error: ${(err as Error)?.message || String(err)}`);
|
||||
return c.text('Bad Request', 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files from the frontend directory (must come after API routes)
|
||||
// Add cache headers for static assets
|
||||
app.use('/assets/*', async (c, next) => {
|
||||
await next();
|
||||
// Cache assets for 1 year
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
});
|
||||
app.use('/*', serveStatic({ root: './frontend' }));
|
||||
|
||||
// Error handler
|
||||
app.onError((err, c) => {
|
||||
logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
|
||||
return c.text('Internal Server Error', 500);
|
||||
});
|
||||
|
||||
// Default to 3005 for test instance; can be overridden by Environment=PORT
|
||||
const PORT = Number(process.env.PORT || 3005);
|
||||
|
||||
logLine('logs/server.log', `Starting frontend server (Redis version) on port ${PORT}`);
|
||||
|
||||
// Warm up cache on startup
|
||||
(async () => {
|
||||
try {
|
||||
if (isRedisConnected()) {
|
||||
logLine('logs/server.log', 'Warming up jobs cache...');
|
||||
const result = await fetchAllJobsFromPocketBase('');
|
||||
await setCache('jobs:all', result, 1800);
|
||||
logLine('logs/server.log', `✓ Cache warmed with ${result.items.length} jobs`);
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Cache warmup failed: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
})();
|
||||
|
||||
// Background refresh every 5 minutes to keep cache hot
|
||||
setInterval(async () => {
|
||||
try {
|
||||
if (isRedisConnected()) {
|
||||
await refreshJobsCache('jobs:all', 1800, '');
|
||||
}
|
||||
} catch (err) {
|
||||
logLine('logs/error.log', `Periodic refresh error: ${(err as Error)?.message || String(err)}`);
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
|
||||
// Graceful shutdown logging
|
||||
const shutdown = (signal: string) => {
|
||||
logLine('logs/server.log', `Shutting down due to ${signal}`);
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
|
||||
export default {
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
@@ -1,600 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "job-info",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.8.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"tailwind": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.3.4", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"ajv": ["ajv@6.10.0", "", { "dependencies": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg=="],
|
||||
|
||||
"amqplib": ["amqplib@0.5.2", "", { "dependencies": { "bitsyntax": "~0.0.4", "bluebird": "^3.4.6", "buffer-more-ints": "0.0.2", "readable-stream": "1.x >=1.1.9", "safe-buffer": "^5.0.1" } }, "sha512-l9mCs6LbydtHqRniRwYkKdqxVa6XMz3Vw1fh+2gJaaVgTM6Jk3o8RccAKWKtlhT1US5sWrFh+KKxsVUALURSIA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"app-root-path": ["app-root-path@2.1.0", "", {}, "sha512-z5BqVjscbjmJBybKlICogJR2jCr2q/Ixu7Pvui5D4y97i7FLsJlvEG9XOR/KJRlkxxZz7UaaS2TMwQh1dRJ2dA=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"array.prototype.reduce": ["array.prototype.reduce@1.0.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-array-method-boxes-properly": "^1.0.0", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "is-string": "^1.1.1" } }, "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw=="],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"asn1": ["asn1@0.2.3", "", {}, "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="],
|
||||
|
||||
"async-retry": ["async-retry@1.2.3", "", { "dependencies": { "retry": "0.12.0" } }, "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"babel-runtime": ["babel-runtime@6.26.0", "", { "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"bitsyntax": ["bitsyntax@0.0.4", "", { "dependencies": { "buffer-more-ints": "0.0.2" } }, "sha512-Pav3HSZXD2NLQOWfJldY3bpJLt8+HS2nUo5Z1bLLmHg2vCE/cM1qfEvNjlYo7GgYQPneNr715Bh42i01ZHZPvw=="],
|
||||
|
||||
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||
|
||||
"body-parser": ["body-parser@1.18.3", "", { "dependencies": { "bytes": "3.0.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", "http-errors": "~1.6.3", "iconv-lite": "0.4.23", "on-finished": "~2.3.0", "qs": "6.5.2", "raw-body": "2.3.3", "type-is": "~1.6.16" } }, "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-more-ints": ["buffer-more-ints@0.0.2", "", {}, "sha512-PDgX2QJgUc5+Jb2xAoBFP5MxhtVUmZHR33ak+m/SDxRdCrbnX1BggRIaxiW7ImwfmO4iJeCQKN18ToSXWGjYkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||
|
||||
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"commands-events": ["commands-events@1.0.4", "", { "dependencies": { "@babel/runtime": "7.2.0", "formats": "1.0.0", "uuidv4": "2.0.0" } }, "sha512-HdP/+1Anoc7z+6L2h7nd4Imz54+LW+BjMGt30riBZrZ3ZeP/8el93wD8Jj8ltAaqVslqNgjX6qlhSBJwuDSmpg=="],
|
||||
|
||||
"comparejs": ["comparejs@1.0.0", "", {}, "sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw=="],
|
||||
|
||||
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
|
||||
|
||||
"compression": ["compression@1.7.3", "", { "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.14", "debug": "2.6.9", "on-headers": "~1.0.1", "safe-buffer": "5.1.2", "vary": "~1.1.2" } }, "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="],
|
||||
|
||||
"content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="],
|
||||
|
||||
"cookie": ["cookie@0.3.1", "", {}, "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
|
||||
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"crypto2": ["crypto2@2.0.0", "", { "dependencies": { "babel-runtime": "6.26.0", "node-rsa": "0.4.2", "util.promisify": "1.0.0" } }, "sha512-jdXdAgdILldLOF53md25FiQ6ybj2kUFTiRjs7msKTUoZrzgT/M1FPX5dYGJjbbwFls+RJIiZxNTC02DE/8y0ZQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"datasette": ["datasette@1.0.1", "", { "dependencies": { "comparejs": "1.0.0", "eventemitter2": "5.0.1", "lodash": "4.17.5" } }, "sha512-aJdlCBToEJUP4M57r67r4V6tltwGKa3qetnjpBtXYIlqbX9tM9jsoDMxb4xd9AGjpp3282oHRmqI5Z8TVAU0Mg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"draht": ["draht@1.0.1", "", { "dependencies": { "eventemitter2": "5.0.1" } }, "sha512-yNNHL864dniNmIE9ZKD++mKypiAUAvVZtyV0QrbXH/ak3ebzFqo5xsmRBRqV8pZVhImOSBiyq500Wcmrf44zAg=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
|
||||
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter2": ["eventemitter2@5.0.1", "", {}, "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg=="],
|
||||
|
||||
"express": ["express@4.16.4", "", { "dependencies": { "accepts": "~1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.4", "qs": "6.5.2", "range-parser": "~1.2.0", "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", "statuses": "~1.4.0", "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.1.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "statuses": "~1.4.0", "unpipe": "~1.0.0" } }, "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg=="],
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"flaschenpost": ["flaschenpost@1.1.3", "", { "dependencies": { "@babel/runtime": "7.2.0", "app-root-path": "2.1.0", "babel-runtime": "6.26.0", "chalk": "2.4.1", "find-root": "1.1.0", "lodash": "4.17.11", "moment": "2.22.2", "processenv": "1.1.0", "split2": "3.0.0", "stack-trace": "0.0.10", "stringify-object": "3.3.0", "untildify": "3.0.3", "util.promisify": "1.0.0", "varname": "2.0.3" }, "bin": { "flaschenpost-uncork": "dist/bin/flaschenpost-uncork.js", "flaschenpost-normalize": "dist/bin/flaschenpost-normalize.js" } }, "sha512-1VAYPvDsVBGFJyUrOa/6clnJwZYC3qVq9nJLcypy6lvaaNbo1wOQiH8HQ+4Fw/k51pVG7JHzSf5epb8lmIW86g=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"formats": ["formats@1.0.0", "", {}, "sha512-For0Y8egwEK96JgJo4NONErPhtl7H2QzeB2NYGmzeGeJ8a1JZqPgLYOtM3oJRCYhmgsdDFd6KGRYyfe37XY4Yg=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hase": ["hase@2.0.0", "", { "dependencies": { "@babel/runtime": "7.1.2", "amqplib": "0.5.2" } }, "sha512-L83pBR/oZvQQNjv4kw9aUpTqBxERPiY7B42jsmkt1VDeUaRVhYkEIKzkCqrppjtxHe2EZqzZJzuhMXsWsxYIsw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.23", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
|
||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||
|
||||
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||
|
||||
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
|
||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||
|
||||
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||
|
||||
"json-lines": ["json-lines@1.0.0", "", { "dependencies": { "timer2": "1.0.0" } }, "sha512-ytuLZb4RBQb3bTRsG/QBenyIo5oHLpjeCVph3s2NnoAsZE9K6h+uR+OWpEOWV1UeHdX63tYctGppBpGAc+JNMA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@8.5.0", "", { "dependencies": { "jws": "^3.2.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": "^5.6.0" } }, "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"jws": ["jws@3.2.3", "", { "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g=="],
|
||||
|
||||
"limes": ["limes@2.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "jsonwebtoken": "8.5.0" } }, "sha512-evWD0pnTgPX7QueaSoJl5JBUL30T1ZVzo34ke97tIKmeagqhBTYK/JkKL0vtG3MpNApw8ZY9TlbybfwEz9knBA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"lusca": ["lusca@1.6.1", "", { "dependencies": { "tsscmp": "^1.0.5" } }, "sha512-+JzvUMH/rsE/4XfHdDOl70bip0beRcHSviYATQM0vtls59uVtdn1JMu4iD7ZShBpAmFG8EnaA+PrYG9sECMIOQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@1.0.1", "", {}, "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="],
|
||||
|
||||
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["mime@1.4.1", "", { "bin": { "mime": "cli.js" } }, "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"moment": ["moment@2.22.2", "", {}, "sha512-LRvkBHaJGnrcWvqsElsOhHCzj8mU39wLx5pQ0pc6s153GynCTsPdGdqsVNKAQD9sKnWj11iF7TZx9fpLwdD3fw=="],
|
||||
|
||||
"morgan": ["morgan@1.9.1", "", { "dependencies": { "basic-auth": "~2.0.0", "debug": "2.6.9", "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" } }, "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"nocache": ["nocache@2.0.0", "", {}, "sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"node-rsa": ["node-rsa@0.4.2", "", { "dependencies": { "asn1": "0.2.3" } }, "sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA=="],
|
||||
|
||||
"node-statsd": ["node-statsd@0.1.1", "", {}, "sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"object.getownpropertydescriptors": ["object.getownpropertydescriptors@2.1.9", "", { "dependencies": { "array.prototype.reduce": "^1.0.8", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "gopd": "^1.2.0", "safe-array-concat": "^1.1.3" } }, "sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g=="],
|
||||
|
||||
"on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
||||
"on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"partof": ["partof@1.0.0", "", {}, "sha512-+TXdhKCySpJDynCxgAPoGVyAkiK3QPusQ63/BdU5t68QcYzyU6zkP/T7F3gkMQBVUYqdWEADKa6Kx5zg8QIKrg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.7", "", {}, "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"processenv": ["processenv@1.1.0", "", { "dependencies": { "babel-runtime": "6.26.0" } }, "sha512-SymqIsn8GjEUy8nG7HiyEjgbfk1xFosRIakUX1NHLpriq3vVpKniGrr9RdMWCaGYWByIovbRt2f/WvmP/IOApQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.3.3", "", { "dependencies": { "bytes": "3.0.0", "http-errors": "1.6.3", "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.12.1", "", {}, "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"send": ["send@0.16.2", "", { "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" } }, "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw=="],
|
||||
|
||||
"serve-static": ["serve-static@1.13.2", "", { "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.2", "send": "0.16.2" } }, "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="],
|
||||
|
||||
"sha-1": ["sha-1@0.1.1", "", {}, "sha512-dexizf3hB7d4Jq6Cd0d/NYQiqgEqIfZIpuMfwPfvSb6h06DZKmHyUe55jYwpHC12R42wpqXO6ouhiBpRzIcD/g=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@3.0.0", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@1.4.0", "", {}, "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="],
|
||||
|
||||
"stethoskop": ["stethoskop@1.0.0", "", { "dependencies": { "node-statsd": "0.1.1" } }, "sha512-4JnZ+UmTs9SFfDjSHFlD/EoXcb1bfwntkt4h1ipNGrpxtRzmHTxOmdquCJvIrVu608Um7a09cGX0ZSOSllWJNQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
|
||||
|
||||
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"tailwind": ["tailwind@4.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "ajv": "6.10.0", "app-root-path": "2.1.0", "async-retry": "1.2.3", "body-parser": "1.18.3", "commands-events": "1.0.4", "compression": "1.7.3", "content-type": "1.0.4", "cors": "2.8.5", "crypto2": "2.0.0", "datasette": "1.0.1", "draht": "1.0.1", "express": "4.16.4 ", "flaschenpost": "1.1.3", "hase": "2.0.0", "json-lines": "1.0.0", "limes": "2.0.0", "lodash": "4.17.11", "lusca": "1.6.1", "morgan": "1.9.1", "nocache": "2.0.0", "partof": "1.0.0", "processenv": "1.1.0", "stethoskop": "1.0.0", "timer2": "1.0.0", "uuidv4": "3.0.1", "ws": "6.2.0" } }, "sha512-LlUNoD/5maFG1h5kQ6/hXfFPdcnYw+1Z7z+kUD/W/E71CUMwcnrskxiBM8c3G8wmPsD1VvCuqGYMHviI8+yrmg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"timer2": ["timer2@1.0.0", "", {}, "sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg=="],
|
||||
|
||||
"tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
|
||||
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"untildify": ["untildify@3.0.3", "", {}, "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"util.promisify": ["util.promisify@1.0.0", "", { "dependencies": { "define-properties": "^1.1.2", "object.getownpropertydescriptors": "^2.0.3" } }, "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA=="],
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
|
||||
"uuidv4": ["uuidv4@3.0.1", "", { "dependencies": { "uuid": "3.3.2" } }, "sha512-PPzksdWRl2a5C9hrs3OOYrArTeyoR0ftJ3jtOy+BnVHkT2UlrrzPNt9nTdiGuxmQItHM/AcTXahwZZC57Njojg=="],
|
||||
|
||||
"varname": ["varname@2.0.3", "", {}, "sha512-+DofT9mJAUALhnr9ipZ5Z2icwaEZ7DAajOZT4ffXy3MQqnXtG3b7atItLQEJCkfcJTOf9WcsywneOEibD4eqJg=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"ws": ["ws@6.2.0", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w=="],
|
||||
|
||||
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||
|
||||
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"commands-events/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"commands-events/uuidv4": ["uuidv4@2.0.0", "", { "dependencies": { "sha-1": "0.1.1", "uuid": "3.3.2" } }, "sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"datasette/lodash": ["lodash@4.17.5", "", {}, "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"flaschenpost/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"hase/@babel/runtime": ["@babel/runtime@7.1.2", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg=="],
|
||||
|
||||
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -0,0 +1,313 @@
|
||||
# Mode2Svelte - Advanced SvelteKit Auth Testing Suite
|
||||
|
||||
An enhanced SvelteKit-based testing environment for advanced authentication workflows with Microsoft Azure MSAL and PocketBase. This is an extended implementation of Mode1Svelte with additional testing capabilities.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: SvelteKit 2.x with TypeScript
|
||||
- **Backend**: Node.js Adapter (production-ready)
|
||||
- **Styling**: Tailwind CSS 4
|
||||
- **Authentication**:
|
||||
- Azure MSAL Node (@azure/msal-node) - Microsoft Graph token management
|
||||
- PocketBase - User OAuth2 and token validation
|
||||
- **Build Tool**: Vite (integrated with SvelteKit)
|
||||
- **Runtime**: Node.js 18+
|
||||
|
||||
## Features
|
||||
|
||||
### ✅ Frontend Authentication (PocketBase)
|
||||
- OAuth2 login initialization with Microsoft
|
||||
- Auth state management and retrieval
|
||||
- Token refresh and validation
|
||||
- Session persistence
|
||||
- Automatic logout on invalid tokens
|
||||
|
||||
### ✅ Backend Token Management (Microsoft Graph)
|
||||
- App-only token acquisition via MSAL
|
||||
- Automatic token caching with 60-second expiration buffer
|
||||
- Token refresh on demand
|
||||
- JWT payload inspection
|
||||
|
||||
### ✅ Token Validation & Comparison
|
||||
- PocketBase user token validation
|
||||
- User record retrieval from tokens
|
||||
- Side-by-side token comparison (Graph vs PocketBase)
|
||||
- Token payload analysis
|
||||
|
||||
### ✅ Interactive Testing Dashboard
|
||||
- Real-time authentication status
|
||||
- Visual token testing interface
|
||||
- API endpoint testing
|
||||
- Response inspection
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Mode1Svelte/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ └── auth/
|
||||
│ │ ├── 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
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd Mode1Svelte
|
||||
npm install
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Ensure your `.env` file or environment variables are set:
|
||||
|
||||
```bash
|
||||
export CLIENT_ID="<Azure Client ID>"
|
||||
export TENANT_ID="<Azure Tenant ID>"
|
||||
export CLIENT_SECRET="<Azure Client Secret>"
|
||||
export PB_URL="<PocketBase URL>"
|
||||
export PB_DB="<PocketBase Database URL>"
|
||||
export PORT=5173
|
||||
```
|
||||
|
||||
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
|
||||
npm run dev
|
||||
```
|
||||
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
|
||||
|
||||
### GET `/api/auth/config`
|
||||
Returns frontend-safe configuration for PocketBase OAuth setup.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"pbUrl": "https://pocketbase.example.com",
|
||||
"provider": "microsoft",
|
||||
"collection": "Users"
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/api/auth/graph/token`
|
||||
Fetches a Microsoft Graph token for backend API access.
|
||||
|
||||
**Response:**
|
||||
```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": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/validate-pb-token`
|
||||
Validates a PocketBase user token and retrieves user information.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"pbToken": "user_token_string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"user": {
|
||||
"id": "user_id",
|
||||
"email": "user@example.com",
|
||||
"name": "User Name"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/refresh-graph`
|
||||
Clears the cached Graph token and acquires a new one.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"refreshed": true,
|
||||
"expiresOn": "2026-01-23T02:15:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/compare-tokens`
|
||||
Compares Graph token (backend) and PocketBase token (user) to demonstrate their differences.
|
||||
|
||||
**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.
|
||||
Generated
+2823
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "mode2svelte",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3005",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "PORT=3005 node build/index.js",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.2",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.45.6",
|
||||
"svelte-check": "^4.3.4",
|
||||
"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 |
@@ -0,0 +1,204 @@
|
||||
import { config } from 'dotenv';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import PocketBase from 'pocketbase';
|
||||
import type { GraphTokenCache, BackendAuthConfig } from './types';
|
||||
|
||||
// Load environment variables from shared secrets directory
|
||||
config({ path: '/home/admin/secrets/.env' });
|
||||
|
||||
/**
|
||||
* Configuration Manager
|
||||
* Exposes frontend-safe configuration loaded from environment
|
||||
*/
|
||||
export class AuthConfigManager {
|
||||
/**
|
||||
* Get frontend configuration (safe to expose to browser)
|
||||
*/
|
||||
static getFrontendConfig() {
|
||||
return {
|
||||
pbUrl: process.env.PB_URL!,
|
||||
provider: 'microsoft',
|
||||
collection: 'Users',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all backend secrets (never expose to frontend)
|
||||
*/
|
||||
static getBackendConfig() {
|
||||
return {
|
||||
clientId: process.env.CLIENT_ID!,
|
||||
tenantId: process.env.TENANT_ID!,
|
||||
clientSecret: process.env.CLIENT_SECRET!,
|
||||
pbDb: process.env.PB_DB!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Graph Token Management (Backend)
|
||||
* Handles token acquisition and caching for backend Graph API calls
|
||||
*/
|
||||
export class GraphTokenManager {
|
||||
private cca: ConfidentialClientApplication;
|
||||
private cache: GraphTokenCache | null = null;
|
||||
private config: Required<BackendAuthConfig>;
|
||||
|
||||
constructor(config: BackendAuthConfig) {
|
||||
this.config = {
|
||||
clientId: config.clientId || process.env.CLIENT_ID || '',
|
||||
tenantId: config.tenantId || process.env.TENANT_ID || '',
|
||||
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
|
||||
};
|
||||
|
||||
this.cca = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId: this.config.clientId,
|
||||
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
|
||||
clientSecret: this.config.clientSecret,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Graph token (from cache or acquire new)
|
||||
*/
|
||||
async getToken(): Promise<string> {
|
||||
const now = Date.now();
|
||||
|
||||
// Check cache validity (with 60s buffer for expiration)
|
||||
if (this.cache && this.cache.expiresOn - 60000 > now) {
|
||||
return this.cache.token;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.cca.acquireTokenByClientCredential({
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
});
|
||||
|
||||
if (!result?.accessToken) {
|
||||
throw new Error('Failed to acquire Graph token');
|
||||
}
|
||||
|
||||
const expiresOn = result.expiresOn
|
||||
? new Date(result.expiresOn).getTime()
|
||||
: now + 55 * 60 * 1000; // Default 55 minutes
|
||||
|
||||
this.cache = {
|
||||
token: result.accessToken,
|
||||
expiresOn,
|
||||
};
|
||||
|
||||
return result.accessToken;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new Error(`Failed to acquire Graph token: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token with expiration info
|
||||
*/
|
||||
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
|
||||
const token = await this.getToken();
|
||||
const expiresOn = this.cache?.expiresOn || Date.now();
|
||||
return {
|
||||
token,
|
||||
expiresOnISO: new Date(expiresOn).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is cached and valid
|
||||
*/
|
||||
isTokenValid(): boolean {
|
||||
if (!this.cache) return false;
|
||||
const now = Date.now();
|
||||
return this.cache.expiresOn - 60000 > now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache (force refresh on next call)
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PocketBase Token Validation (Backend)
|
||||
* Validates and uses user PocketBase tokens
|
||||
*/
|
||||
export class PocketBaseValidator {
|
||||
private pb: PocketBase;
|
||||
|
||||
constructor(pbUrl?: string) {
|
||||
this.pb = new PocketBase(pbUrl || process.env.PB_DB!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user token and validate it
|
||||
*/
|
||||
async validateUserToken(token: string): Promise<boolean> {
|
||||
try {
|
||||
this.pb.authStore.save(token, null);
|
||||
await this.pb.collection('Users').authRefresh();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Token validation failed:', error instanceof Error ? error.message : error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user record from token
|
||||
*/
|
||||
async getUserRecord(token: string): Promise<any> {
|
||||
try {
|
||||
this.pb.authStore.save(token, null);
|
||||
const record = this.pb.authStore.record || this.pb.authStore.model;
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Failed to get user record:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined backend auth manager
|
||||
*/
|
||||
export class BackendAuth {
|
||||
graphTokenManager: GraphTokenManager;
|
||||
pbValidator: PocketBaseValidator;
|
||||
|
||||
constructor(config: BackendAuthConfig, pbUrl?: string) {
|
||||
this.graphTokenManager = new GraphTokenManager(config);
|
||||
this.pbValidator = new PocketBaseValidator(pbUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to validate PocketBase token in requests
|
||||
* Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next))
|
||||
*/
|
||||
async validateTokenMiddleware(c: any, next: any): Promise<any> {
|
||||
const token = c.req.header('Authorization')?.replace('Bearer ', '') ||
|
||||
(await c.req.json().catch(() => ({})))?.pbToken;
|
||||
|
||||
if (token) {
|
||||
const isValid = await this.pbValidator.validateUserToken(token);
|
||||
if (!isValid) {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import type { AuthConfig, AuthState, AuthCallbacks } from './types';
|
||||
|
||||
/**
|
||||
* PocketBase OAuth2 Frontend Module
|
||||
* Handles user authentication and token management
|
||||
*/
|
||||
export class PocketBaseAuth {
|
||||
private pb: PocketBase;
|
||||
private config: Required<AuthConfig>;
|
||||
private callbacks: AuthCallbacks;
|
||||
private state: AuthState;
|
||||
|
||||
constructor(config: AuthConfig, callbacks?: Partial<AuthCallbacks>) {
|
||||
this.config = {
|
||||
pbUrl: config.pbUrl!,
|
||||
collection: config.collection || 'Users',
|
||||
provider: config.provider || 'microsoft',
|
||||
loginContainerId: config.loginContainerId || 'loginContainer',
|
||||
userDisplayNameId: config.userDisplayNameId || 'userDisplayName',
|
||||
userEmailId: config.userEmailId || 'userEmailValue',
|
||||
loginBtnId: config.loginBtnId || 'loginBtn',
|
||||
loginErrorId: config.loginErrorId || 'loginError',
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
onAuthSuccess: callbacks?.onAuthSuccess || (() => {}),
|
||||
onAuthFailure: callbacks?.onAuthFailure || (() => {}),
|
||||
onTokenUpdate: callbacks?.onTokenUpdate || (() => {}),
|
||||
onUiUpdate: callbacks?.onUiUpdate || (() => {}),
|
||||
};
|
||||
|
||||
this.pb = new PocketBase(this.config.pbUrl);
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize auth module
|
||||
*/
|
||||
private init(): void {
|
||||
this.updateAuthUI();
|
||||
if (this.pb.authStore.isValid && this.pb.authStore.token) {
|
||||
this.ensureUserLogged();
|
||||
}
|
||||
this.setupLoginButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup login button click handler
|
||||
*/
|
||||
private setupLoginButton(): void {
|
||||
console.log('[Auth] Setting up login button with id:', this.config.loginBtnId);
|
||||
|
||||
// Retry finding button in case component hasn't rendered yet
|
||||
let attempts = 0;
|
||||
const setupInterval = setInterval(() => {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId);
|
||||
console.log(`[Auth] Attempt ${attempts + 1}: Looking for button, found:`, !!loginBtn);
|
||||
|
||||
if (loginBtn) {
|
||||
clearInterval(setupInterval);
|
||||
// Remove any existing listeners to avoid duplicates
|
||||
const newBtn = loginBtn.cloneNode(true) as HTMLElement;
|
||||
loginBtn.parentNode?.replaceChild(newBtn, loginBtn);
|
||||
|
||||
newBtn.addEventListener('click', async (e) => {
|
||||
console.log('[Auth] Login button clicked!');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await this.handleLogin();
|
||||
}, false);
|
||||
console.log(`✓ Login button listener attached (attempt ${attempts + 1})`);
|
||||
} else if (attempts++ > 100) {
|
||||
// Give up after 10 seconds (100 * 100ms)
|
||||
clearInterval(setupInterval);
|
||||
console.error(`Login button with id "${this.config.loginBtnId}" not found after 10 seconds. All elements:`,
|
||||
Array.from(document.querySelectorAll('[id*="login"]')).map(e => e.id));
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login button click
|
||||
*/
|
||||
private async handleLogin(): Promise<void> {
|
||||
console.log('[Auth] handleLogin called');
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement;
|
||||
|
||||
if (!loginBtn) {
|
||||
console.error('[Auth] Login button not found in handleLogin!');
|
||||
return;
|
||||
}
|
||||
if (!loginError) {
|
||||
console.error('[Auth] Login error element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Auth] Starting login flow...');
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Checking session...';
|
||||
loginError.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// Try to use existing token first
|
||||
console.log('[Auth] Checking for existing token...');
|
||||
const hadToken = await this.ensureUserLogged();
|
||||
if (hadToken) {
|
||||
console.log('[Auth] Found valid existing token, using it');
|
||||
this.resetLoginButton();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise perform OAuth
|
||||
console.log('[Auth] No valid token, starting OAuth flow...');
|
||||
loginBtn.textContent = 'Logging in...';
|
||||
console.log('[Auth] Calling authWithOAuth2 with provider:', this.config.provider);
|
||||
const authData = await this.pb.collection(this.config.collection).authWithOAuth2({
|
||||
provider: this.config.provider,
|
||||
});
|
||||
|
||||
console.log('[Auth] OAuth successful, updating state');
|
||||
this.updateState(authData);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onAuthSuccess?.(this.state);
|
||||
|
||||
console.log('✓ Logged in with', this.config.provider);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('[Auth] Login failed:', message, error);
|
||||
loginError.textContent = `Login failed: ${message}`;
|
||||
loginError.classList.remove('hidden');
|
||||
this.callbacks.onAuthFailure?.(error);
|
||||
} finally {
|
||||
this.resetLoginButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user is logged in (check existing token)
|
||||
*/
|
||||
async ensureUserLogged(): Promise<boolean> {
|
||||
if (!this.pb.authStore.isValid || !this.pb.authStore.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refresh = await this.pb.collection(this.config.collection).authRefresh();
|
||||
this.updateState(refresh);
|
||||
this.updateAuthUI();
|
||||
this.callbacks.onTokenUpdate?.(this.state);
|
||||
console.log('✓ Token refreshed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Existing token invalid, clearing auth');
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update internal auth state
|
||||
*/
|
||||
private updateState(data: any): void {
|
||||
if (!data) {
|
||||
this.state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const record = data.record || this.pb.authStore.record || this.pb.authStore.model;
|
||||
const meta = data.meta || {};
|
||||
const model = this.pb.authStore.model;
|
||||
|
||||
this.state = {
|
||||
isAuthenticated: true,
|
||||
user: {
|
||||
id: record?.id || model?.id || 'unknown',
|
||||
name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User',
|
||||
email: record?.email || model?.email || meta?.email || '(no email)',
|
||||
},
|
||||
token: this.pb.authStore.token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI based on auth state
|
||||
*/
|
||||
updateAuthUI(): void {
|
||||
const loginContainer = document.getElementById(this.config.loginContainerId);
|
||||
const userDisplayName = document.getElementById(this.config.userDisplayNameId);
|
||||
const userEmail = document.getElementById(this.config.userEmailId);
|
||||
|
||||
if (!loginContainer) return;
|
||||
|
||||
if (this.pb.authStore.isValid) {
|
||||
loginContainer.classList.add('hidden');
|
||||
if (this.state.user) {
|
||||
if (userDisplayName) userDisplayName.textContent = this.state.user.name;
|
||||
if (userEmail) userEmail.textContent = this.state.user.email;
|
||||
}
|
||||
} else {
|
||||
loginContainer.classList.remove('hidden');
|
||||
const loginError = document.getElementById(this.config.loginErrorId);
|
||||
if (loginError) loginError.classList.add('hidden');
|
||||
}
|
||||
|
||||
this.callbacks.onUiUpdate?.(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check token status (for verification/display)
|
||||
*/
|
||||
async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> {
|
||||
const pbToken = !!this.pb.authStore.token;
|
||||
return { pbToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current auth state
|
||||
*/
|
||||
getAuthState(): AuthState {
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PocketBase instance (for direct usage if needed)
|
||||
*/
|
||||
getPocketBase(): PocketBase {
|
||||
return this.pb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout(): void {
|
||||
this.pb.authStore.clear();
|
||||
this.updateState(null);
|
||||
this.updateAuthUI();
|
||||
console.log('✓ Logged out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset login button to initial state
|
||||
*/
|
||||
private resetLoginButton(): void {
|
||||
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
|
||||
if (loginBtn) {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Login with Microsoft';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick init function - fetches config from backend
|
||||
*/
|
||||
export async function initPocketBaseAuth(
|
||||
backendUrl: string,
|
||||
callbacks?: Partial<AuthCallbacks>
|
||||
): Promise<PocketBaseAuth> {
|
||||
// Fetch frontend config from backend
|
||||
const response = await fetch(`${backendUrl}/api/auth/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch auth configuration from backend');
|
||||
}
|
||||
const config = await response.json();
|
||||
|
||||
const auth = new PocketBaseAuth(config, callbacks);
|
||||
return auth;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Frontend Auth Configuration
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
pbUrl: string; // PocketBase URL (required)
|
||||
collection?: string;
|
||||
provider?: string;
|
||||
loginContainerId?: string;
|
||||
userDisplayNameId?: string;
|
||||
userEmailId?: string;
|
||||
loginBtnId?: string;
|
||||
loginErrorId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend configuration provided by backend
|
||||
*/
|
||||
export interface FrontendConfig {
|
||||
pbUrl: string;
|
||||
provider?: string;
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend Auth Configuration
|
||||
*/
|
||||
export interface BackendAuthConfig {
|
||||
clientId?: string;
|
||||
tenantId?: string;
|
||||
clientSecret?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth state object
|
||||
*/
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
} | null;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph token cache object
|
||||
*/
|
||||
export interface GraphTokenCache {
|
||||
token: string;
|
||||
expiresOn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth event callbacks
|
||||
*/
|
||||
export interface AuthCallbacks {
|
||||
onAuthSuccess?: (state: AuthState) => void;
|
||||
onAuthFailure?: (error: any) => void;
|
||||
onTokenUpdate?: (state: AuthState) => void;
|
||||
onUiUpdate?: (state: AuthState) => void;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,62 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
/**
|
||||
* Proxy endpoint to fetch file content from SharePoint
|
||||
* Bypasses CORS restrictions by using backend authentication
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ request, url }) => {
|
||||
try {
|
||||
// Get Graph token from request header or environment
|
||||
const headerToken = request.headers.get('x-graph-token') || '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
|
||||
if (!token) {
|
||||
return json({ error: 'GRAPH_TOKEN missing' }, { status: 500 });
|
||||
}
|
||||
|
||||
const driveId = url.searchParams.get('driveId');
|
||||
const itemId = url.searchParams.get('itemId');
|
||||
|
||||
if (!driveId || !itemId) {
|
||||
return json({ error: 'driveId and itemId required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch from Microsoft Graph API
|
||||
const graphUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await fetch(graphUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
console.error(`[file-content] Graph API error: ${res.status} - ${text}`);
|
||||
return json({ error: `Graph ${res.status}: ${text}` }, { status: 502 });
|
||||
}
|
||||
|
||||
// Get the content type and disposition headers
|
||||
const contentType = res.headers.get('content-type') || 'application/octet-stream';
|
||||
const contentDisposition = res.headers.get('content-disposition') || '';
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
};
|
||||
|
||||
if (contentDisposition) {
|
||||
headers['Content-Disposition'] = contentDisposition;
|
||||
}
|
||||
|
||||
// Return the file as a response
|
||||
const buffer = await res.arrayBuffer();
|
||||
return new Response(buffer, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error('[file-content] Error:', message);
|
||||
return json({ error: message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
/**
|
||||
* Proxy endpoint to fetch and convert Word documents to PDF
|
||||
* Uses Microsoft Graph API with ?format=pdf parameter
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ request, url }) => {
|
||||
try {
|
||||
// Get Graph token from request header or environment
|
||||
const headerToken = request.headers.get('x-graph-token') || '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
|
||||
if (!token) {
|
||||
return json({ error: 'GRAPH_TOKEN missing' }, { status: 500 });
|
||||
}
|
||||
|
||||
const driveId = url.searchParams.get('driveId');
|
||||
const itemId = url.searchParams.get('itemId');
|
||||
|
||||
if (!driveId || !itemId) {
|
||||
return json({ error: 'driveId and itemId required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Use Graph API to convert to PDF format
|
||||
const graphUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`;
|
||||
const res = await fetch(graphUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
console.error(`[file-pdf] Conversion failed: ${res.status} - ${text}`);
|
||||
return json({ error: `PDF conversion failed: ${res.status}` }, { status: 502 });
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
};
|
||||
|
||||
// Update filename to .pdf if present
|
||||
const contentDisposition = res.headers.get('content-disposition');
|
||||
if (contentDisposition) {
|
||||
headers['Content-Disposition'] = contentDisposition.replace(/\.(docx?)/gi, '.pdf');
|
||||
}
|
||||
|
||||
console.log(`[file-pdf] Successfully converted: driveId=${driveId}, itemId=${itemId}`);
|
||||
|
||||
const buffer = await res.arrayBuffer();
|
||||
return new Response(buffer, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error('[file-pdf] Error:', message);
|
||||
return json({ error: message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
|
||||
|
||||
/**
|
||||
* Helper to encode SharePoint links for Graph API /shares endpoint
|
||||
*/
|
||||
function b64Url(input: string) {
|
||||
return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to fetch JSON from Graph API
|
||||
*/
|
||||
async function fetchJson(url: string, token: string) {
|
||||
console.log('📁 Fetching:', url);
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
const err: any = new Error(`Graph ${res.status}: ${text}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a SharePoint folder link to driveId and itemId
|
||||
* Handles both short sharing links and direct document library URLs
|
||||
* Works with both RootFolder parameter format and direct path format
|
||||
*/
|
||||
const resolveShareLink = async (link: string, token: string) => {
|
||||
// Decode the link in case it comes pre-encoded from the database
|
||||
const decodedLink = decodeURIComponent(link);
|
||||
console.log('[resolveShareLink] Decoded link:', decodedLink.substring(0, 150));
|
||||
|
||||
// Check if this is a short sharing link (/:f:/ or /:b:/)
|
||||
if (decodedLink.includes('/:f:/') || decodedLink.includes('/:b:/')) {
|
||||
// Short sharing link - use shares API
|
||||
const encoded = `u!${b64Url(decodedLink)}`;
|
||||
console.log('[resolveShareLink] Using shares API, encoded:', encoded);
|
||||
const data = await fetchJson(`${GRAPH_BASE}/shares/${encoded}/driveItem`, token);
|
||||
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
|
||||
} else {
|
||||
// Direct document library URL - parse it and use drive API
|
||||
const url = new URL(decodedLink);
|
||||
const pathMatch = url.hostname.match(/^([^.]+)\.sharepoint\.com$/);
|
||||
if (!pathMatch) throw new Error('Invalid SharePoint URL');
|
||||
|
||||
const hostname = pathMatch[1]; // e.g., 'czflex'
|
||||
|
||||
// Decode pathname to handle spaces properly
|
||||
const decodedPathname = decodeURIComponent(url.pathname);
|
||||
console.log('[resolveShareLink] Decoded pathname:', decodedPathname);
|
||||
|
||||
const sitePath = decodedPathname.split('/Shared Documents')[0]; // e.g., /sites/Team
|
||||
let folderPath: string | null = null;
|
||||
|
||||
// Try RootFolder parameter first (AllItems.aspx format)
|
||||
const rootFolderParam = url.searchParams.get('RootFolder');
|
||||
if (rootFolderParam) {
|
||||
console.log('[resolveShareLink] Found RootFolder parameter');
|
||||
// RootFolder format: /sites/Team/Shared Documents/General/Operations [Server]/...
|
||||
// We need: General/Operations [Server]/...
|
||||
folderPath = rootFolderParam.split('/Shared Documents/')[1];
|
||||
if (!folderPath) throw new Error('Could not parse folder path from RootFolder');
|
||||
} else {
|
||||
// No RootFolder - extract from pathname directly
|
||||
// Format: /sites/Team/Shared Documents/General/Operations [Server]/1 Job Pages/...
|
||||
console.log('[resolveShareLink] No RootFolder, parsing from pathname');
|
||||
const sharedDocsIndex = decodedPathname.indexOf('/Shared Documents/');
|
||||
if (sharedDocsIndex !== -1) {
|
||||
const pathAfterSharedDocs = decodedPathname.substring(sharedDocsIndex + '/Shared Documents/'.length);
|
||||
folderPath = pathAfterSharedDocs;
|
||||
console.log('[resolveShareLink] Extracted folderPath from pathname:', folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!folderPath) {
|
||||
throw new Error('Could not extract folder path from URL');
|
||||
}
|
||||
|
||||
console.log('[resolveShareLink] Hostname:', hostname);
|
||||
console.log('[resolveShareLink] Site path:', sitePath);
|
||||
console.log('[resolveShareLink] Folder path:', folderPath);
|
||||
|
||||
// Get the site ID first using hostname:sitePath format
|
||||
const siteData = await fetchJson(`${GRAPH_BASE}/sites/${hostname}.sharepoint.com:${sitePath}`, token);
|
||||
const siteId = siteData.id;
|
||||
console.log('[resolveShareLink] Site ID:', siteId);
|
||||
|
||||
// Get the default document library drive
|
||||
const driveData = await fetchJson(`${GRAPH_BASE}/sites/${siteId}/drive`, token);
|
||||
const driveId = driveData.id;
|
||||
console.log('[resolveShareLink] Drive ID:', driveId);
|
||||
|
||||
// Get the folder item by path - need to properly encode the path
|
||||
const encodedPath = folderPath
|
||||
.split('/')
|
||||
.map(segment => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
|
||||
console.log('[resolveShareLink] Encoded path for Graph API:', encodedPath);
|
||||
|
||||
const itemData = await fetchJson(
|
||||
`${GRAPH_BASE}/drives/${driveId}/root:/${encodedPath}`,
|
||||
token
|
||||
);
|
||||
|
||||
console.log('[resolveShareLink] Resolved to driveId:', driveId, 'itemId:', itemData.id);
|
||||
return { driveId, itemId: itemData.id };
|
||||
}
|
||||
};
|
||||
|
||||
async function listChildrenRecursive(
|
||||
driveId: string,
|
||||
itemId: string,
|
||||
depth = 0,
|
||||
maxDepth = 5,
|
||||
token: string,
|
||||
): Promise<any[]> {
|
||||
const out: any[] = [];
|
||||
const data = await fetchJson(`${GRAPH_BASE}/drives/${driveId}/items/${itemId}/children`, token);
|
||||
for (const child of data.value || []) {
|
||||
out.push(child);
|
||||
if (child.folder && depth < maxDepth) {
|
||||
const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
|
||||
out.push(...nested);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function filterByCategory(items: any[], category?: string, pdfOnly: boolean = true) {
|
||||
// First filter: show PDF files and Word documents (which will be converted to PDF)
|
||||
let filtered = items;
|
||||
if (pdfOnly) {
|
||||
filtered = items.filter((i) => {
|
||||
if (i.folder) return false;
|
||||
const name = i.name.toLowerCase();
|
||||
const mimeType = i.file?.mimeType?.toLowerCase() || '';
|
||||
// Include PDFs, Word documents, and images
|
||||
return name.endsWith('.pdf') || mimeType.includes('pdf') ||
|
||||
name.endsWith('.doc') || name.endsWith('.docx') || mimeType.includes('word') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') ||
|
||||
name.endsWith('.gif') || name.endsWith('.bmp') || name.endsWith('.tiff') ||
|
||||
name.endsWith('.webp') || mimeType.includes('image');
|
||||
});
|
||||
}
|
||||
|
||||
// Second filter: category filtering (if specified)
|
||||
if (!category) return filtered;
|
||||
const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
|
||||
const lc = (s: string) => (s || '').toLowerCase();
|
||||
const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
|
||||
const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
|
||||
return filtered.filter((i) => {
|
||||
const n = lc(i.name);
|
||||
if (category === 'contracts') return nameHas(n, contractWords);
|
||||
if (category === 'plans') return nameHas(n, planWords);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const GET: RequestHandler = async ({ url, request }) => {
|
||||
try {
|
||||
const link = url.searchParams.get('link');
|
||||
const category = url.searchParams.get('category');
|
||||
|
||||
if (!link) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'link required' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Get Graph token - try header first, then fall back to environment variable
|
||||
const headerToken = request.headers.get('x-graph-token') || '';
|
||||
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
|
||||
const token = headerToken || envToken;
|
||||
|
||||
console.log('[job-files] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
|
||||
console.log('[job-files] Env token:', envToken ? 'SET' : 'NOT SET');
|
||||
console.log('[job-files] Using token:', token ? token.substring(0, 20) + '...' : 'NONE');
|
||||
|
||||
if (!token) {
|
||||
console.log('[job-files] No token found');
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'GRAPH_TOKEN missing' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[job-files] Link:', link);
|
||||
const { driveId, itemId } = await resolveShareLink(link, token);
|
||||
|
||||
// Walk subfolders up to depth 5
|
||||
let items: any[] = [];
|
||||
items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
|
||||
|
||||
// Filter to show only PDFs by default (can be disabled with pdfOnly=false query param)
|
||||
const pdfOnly = url.searchParams.get('pdfOnly') !== 'false';
|
||||
const filtered = filterByCategory(items, category, pdfOnly);
|
||||
console.log(`[job-files] Found ${items.length} total items, ${filtered.length} after filtering`);
|
||||
|
||||
const mapped = filtered.map((i) => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
url: i.webUrl,
|
||||
driveId: i.parentReference?.driveId || driveId,
|
||||
size: i.size,
|
||||
modified: i.lastModifiedDateTime,
|
||||
contentType: i.file?.mimeType,
|
||||
isFolder: Boolean(i.folder),
|
||||
path: i.parentReference?.path || '',
|
||||
}));
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ items: mapped, total: mapped.length, source: 'walk', driveId }),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
} catch (err) {
|
||||
const message = (err as Error)?.message || String(err);
|
||||
const status = (err as any)?.status || 500;
|
||||
console.error('[job-files] ERROR:', message);
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{ status: status, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const PB_DB = process.env.PB_DB || 'https://pocketbase.ccllc.pro';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
try {
|
||||
const pb = new PocketBase(PB_DB);
|
||||
const record = await pb.collection('pbc_1407612689').getOne(params.id!);
|
||||
|
||||
return new Response(JSON.stringify(record), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Job not found',
|
||||
}),
|
||||
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
try {
|
||||
const pb = new PocketBase(PB_DB);
|
||||
const data = await request.json();
|
||||
|
||||
const updated = await pb.collection('pbc_1407612689').update(params.id!, data);
|
||||
|
||||
return new Response(JSON.stringify(updated), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Failed to update job',
|
||||
}),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const PB_DB = process.env.PB_DB || 'https://pocketbase.ccllc.pro';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
try {
|
||||
const pb = new PocketBase(PB_DB);
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const perPage = Number(url.searchParams.get('perPage')) || 30;
|
||||
const sort = url.searchParams.get('sort') || '-Job_Number';
|
||||
|
||||
// Build filter query from parameters
|
||||
const filters: string[] = [];
|
||||
const division = url.searchParams.get('division');
|
||||
const active = url.searchParams.get('active');
|
||||
const estimator = url.searchParams.get('estimator');
|
||||
const status = url.searchParams.get('status');
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
if (division) {
|
||||
filters.push(`Job_Division = "${division}"`);
|
||||
}
|
||||
if (active) {
|
||||
filters.push(`Active = ${active === 'Yes' ? 'true' : 'false'}`);
|
||||
}
|
||||
if (estimator) {
|
||||
filters.push(`Estimator = "${estimator}"`);
|
||||
}
|
||||
if (status) {
|
||||
filters.push(`Job_Status = "${status}"`);
|
||||
}
|
||||
if (search) {
|
||||
filters.push(`(Job_Number ~ "${search}" || Job_Name ~ "${search}" || Company_Client ~ "${search}")`);
|
||||
}
|
||||
|
||||
const filterQuery = filters.length > 0 ? filters.join(' && ') : '';
|
||||
|
||||
const records = await pb.collection('pbc_1407612689').getList(page, perPage, {
|
||||
sort,
|
||||
filter: filterQuery,
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(records), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch jobs',
|
||||
}),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -1,10 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./frontend/**/*.{html,js}',
|
||||
],
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
build: {
|
||||
// Ensure asset paths are relative, not absolute
|
||||
// This fixes proxy routing issues
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'assets/[name]-[hash][extname]'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
# Mode2Test
|
||||
|
||||
Complete OAuth2 authentication system with persistent token management.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Mode2Test/
|
||||
AuthAndToken/ # Everything related to auth and tokens
|
||||
package.json
|
||||
backend/
|
||||
server.ts # Hono server
|
||||
token-service.ts # Token storage & refresh
|
||||
auth-routes.ts # Hono API endpoints
|
||||
frontend/
|
||||
index.html # Main app (requires auth)
|
||||
signin.html # Microsoft OAuth2 login
|
||||
auth-manager.ts # Frontend auth helpers
|
||||
README.md # Component documentation
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
cd Mode2Test/AuthAndToken
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Set environment variables (in `secrets/.env`):
|
||||
```
|
||||
POCKETBASE_URL=https://pocketbase.ccllc.pro
|
||||
```
|
||||
|
||||
3. Run:
|
||||
```bash
|
||||
bun run backend/server.ts
|
||||
```
|
||||
|
||||
4. Open `http://localhost:3005` → redirects to signin.html if not authenticated
|
||||
|
||||
## How It Works
|
||||
|
||||
### Token Lifecycle
|
||||
|
||||
1. **Login**: User signs in via Microsoft OAuth2 → PB and Graph tokens acquired → stored in `tokens.json` → background refresh starts
|
||||
2. **Refresh**: Every 5 minutes, both tokens auto-refresh silently
|
||||
3. **Access**: Frontend/backend call `/api/auth/tokens` to get current state
|
||||
4. **Logout**: User clicks logout → tokens cleared
|
||||
|
||||
### Startup Behavior
|
||||
|
||||
- If `tokens.json` exists with valid PB token: background refresh resumes immediately
|
||||
- If no valid token: frontend redirects to `signin.html` to require login
|
||||
- Tokens are **never deleted** until fresh ones are confirmed available
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /api/auth/status` - Check auth status
|
||||
- `GET /api/auth/tokens` - Get current tokens (requires auth)
|
||||
- `POST /api/auth/login` - Store tokens after OAuth login
|
||||
- `POST /api/auth/logout` - Clear all tokens
|
||||
|
||||
## Tokens
|
||||
|
||||
- **PocketBase Token**: Required. Refreshes automatically every 5 minutes.
|
||||
- **Graph Token**: Optional. Refreshes automatically if available.
|
||||
|
||||
Both stay fresh at all times after initial login—no user intervention needed.
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import { tokenService } from './token-service';
|
||||
|
||||
// Authentication API routes
|
||||
// Endpoints for frontend to check auth status, get tokens, logout
|
||||
// Depends on: tokenService singleton (centralized token management)
|
||||
|
||||
export function createAuthRoutes(): Hono {
|
||||
const router = new Hono();
|
||||
|
||||
// PERMANENT: Check if user is authenticated
|
||||
// Returns current auth status and which tokens are available
|
||||
router.get('/status', async (c: Context) => {
|
||||
const tokens = await tokenService.getTokens();
|
||||
const hasToken = !!tokens?.pbToken;
|
||||
return c.json({
|
||||
authenticated: hasToken,
|
||||
pbTokenValid: hasToken,
|
||||
assumedFresh: hasToken,
|
||||
message: hasToken ? 'Assuming tokens are valid (auto-refresh running)' : 'No PB token yet; login once'
|
||||
});
|
||||
});
|
||||
|
||||
// PERMANENT: Get current tokens
|
||||
// Frontend/backend can call this to get fresh token state
|
||||
// Only returns tokens if PB token is valid (auth required)
|
||||
router.get('/tokens', async (c: Context) => {
|
||||
const tokens = await tokenService.getTokens();
|
||||
return c.json({
|
||||
pbToken: tokens?.pbToken || null,
|
||||
graphToken: tokens?.graphToken || null,
|
||||
pbTokenExpiry: tokens?.pbTokenExpiry || null,
|
||||
graphTokenExpiry: tokens?.graphTokenExpiry || null,
|
||||
lastRefresh: tokens?.lastRefresh || null,
|
||||
assumedFresh: !!tokens?.pbToken
|
||||
});
|
||||
});
|
||||
|
||||
// PERMANENT: Handle login callback from OAuth2
|
||||
// Frontend sends back the PB auth response after successful login
|
||||
// Stores both PB and Graph tokens
|
||||
router.post('/login', async (c: Context) => {
|
||||
try {
|
||||
const body = await c.req.json() as any;
|
||||
const pbToken = body.pbToken;
|
||||
const graphToken = body.graphToken;
|
||||
|
||||
if (!pbToken) {
|
||||
return c.json({ error: 'Missing pbToken' }, 400);
|
||||
}
|
||||
|
||||
await tokenService.saveTokens(pbToken, graphToken);
|
||||
return c.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
authenticated: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Login error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Login failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// PERMANENT: Logout - clear all tokens
|
||||
// Called when user clicks logout
|
||||
router.post('/logout', async (c: Context) => {
|
||||
try {
|
||||
await tokenService.clearTokens();
|
||||
return c.json({
|
||||
success: true,
|
||||
message: 'Logged out',
|
||||
authenticated: false
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Logout error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Logout failed' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import { tokenService } from './token-service';
|
||||
|
||||
// PERMANENT: Jobs API routes
|
||||
// Provides access to job data from PocketBase
|
||||
// Requires authentication (valid PB token)
|
||||
//
|
||||
// Dependencies:
|
||||
// - tokenService for PB token retrieval
|
||||
// - process.env.POCKETBASE_URL for PB API endpoint
|
||||
const JOBS_COLLECTION = 'pbc_1407612689';
|
||||
//
|
||||
// Endpoints:
|
||||
// - GET /api/jobs - List jobs with pagination, filtering, field selection
|
||||
|
||||
export function createJobsRoutes(): Hono {
|
||||
const router = new Hono();
|
||||
|
||||
// GET /api/jobs - Fetch jobs from PocketBase
|
||||
// Query params:
|
||||
// - page: Page number (default 1)
|
||||
// - perPage: Items per page (default 50, max 1000)
|
||||
// - fields: Comma-separated field names to return (optional, for cache optimization)
|
||||
// - filter: PocketBase filter syntax (optional)
|
||||
// - sort: Sort field (default -Job_Number)
|
||||
router.get('/', async (c: Context) => {
|
||||
try {
|
||||
// Get token (assume present after login)
|
||||
const tokens = await tokenService.getTokens();
|
||||
const pbToken = tokens?.pbToken;
|
||||
|
||||
// Parse query params
|
||||
const page = parseInt(c.req.query('page') || '1');
|
||||
const perPage = Math.min(parseInt(c.req.query('perPage') || '50'), 1000);
|
||||
const fields = c.req.query('fields'); // Optional: comma-separated field names
|
||||
const filter = c.req.query('filter'); // Optional: PocketBase filter
|
||||
const sort = c.req.query('sort') || '-Job_Number';
|
||||
|
||||
// Build PocketBase API URL
|
||||
const pbUrl = new URL(`${process.env.POCKETBASE_URL}/api/collections/${JOBS_COLLECTION}/records`);
|
||||
pbUrl.searchParams.set('page', page.toString());
|
||||
pbUrl.searchParams.set('perPage', perPage.toString());
|
||||
pbUrl.searchParams.set('sort', sort);
|
||||
|
||||
if (fields) {
|
||||
pbUrl.searchParams.set('fields', fields);
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
pbUrl.searchParams.set('filter', filter);
|
||||
}
|
||||
|
||||
// Fetch from PocketBase
|
||||
// temporary: dev-only SSL bypass; global rejectUnauthorized already set, keep here as explicit
|
||||
const response = await fetch(pbUrl.toString(), {
|
||||
headers: pbToken ? { 'Authorization': `Bearer ${pbToken}` } : {},
|
||||
// @ts-ignore Bun TLS option
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('PocketBase jobs fetch failed:', response.status, errorText);
|
||||
return c.json({ error: 'Failed to fetch jobs from PocketBase' }, response.status);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return c.json(data);
|
||||
} catch (err) {
|
||||
console.error('Jobs API error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/jobs/:id - Fetch single job by ID
|
||||
router.get('/:id', async (c: Context) => {
|
||||
try {
|
||||
// Check authentication
|
||||
const tokens = await tokenService.getTokens();
|
||||
const pbToken = tokens?.pbToken;
|
||||
|
||||
const jobId = c.req.param('id');
|
||||
const pbUrl = `${process.env.POCKETBASE_URL}/api/collections/${JOBS_COLLECTION}/records/${jobId}`;
|
||||
|
||||
// Fetch from PocketBase
|
||||
const response = await fetch(pbUrl, {
|
||||
headers: pbToken ? { 'Authorization': `Bearer ${pbToken}` } : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return c.json({ error: 'Job not found' }, 404);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return c.json(data);
|
||||
} catch (err) {
|
||||
console.error('Job fetch error:', (err as Error)?.message);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { join } from 'path';
|
||||
import { tokenService } from './token-service';
|
||||
import { createAuthRoutes } from './auth-routes';
|
||||
import { createJobsRoutes } from './jobs-routes';
|
||||
|
||||
// temporary: dev-only SSL bypass for upstream PocketBase cert issues
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
// PERMANENT: Load environment variables
|
||||
// Check if POCKETBASE_URL is set, use default if not
|
||||
if (!process.env.POCKETBASE_URL) {
|
||||
process.env.POCKETBASE_URL = 'https://pocketbase.ccllc.pro';
|
||||
console.log('[Mode2Test] Using default POCKETBASE_URL:', process.env.POCKETBASE_URL);
|
||||
}
|
||||
|
||||
// PERMANENT: Initialize token service and start background refresh
|
||||
// Loads existing tokens from file if available
|
||||
// Starts 5-minute background refresh cycle
|
||||
tokenService.initialize();
|
||||
tokenService.startBackgroundRefresh();
|
||||
|
||||
// Create Hono app
|
||||
const app = new Hono();
|
||||
|
||||
// Mount auth routes
|
||||
app.route('/api/auth', createAuthRoutes());
|
||||
|
||||
// Mount jobs routes
|
||||
app.route('/api/jobs', createJobsRoutes());
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (c) => {
|
||||
return c.text('OK');
|
||||
});
|
||||
|
||||
// Version endpoint
|
||||
app.get('/version', (c) => {
|
||||
return c.json({ version: '1.0.0-beta1' });
|
||||
});
|
||||
|
||||
// PERMANENT: Serve static frontend files
|
||||
// Serves from frontend/ directory
|
||||
const publicDir = join(import.meta.dir, '../frontend');
|
||||
app.use('/*', serveStatic({ root: publicDir }));
|
||||
|
||||
// Start server on port 3005
|
||||
const port = 3005;
|
||||
console.log(`Mode2Test (AuthAndToken) running on http://localhost:${port}`);
|
||||
console.log(`Auth API: /api/auth/*`);
|
||||
console.log(`SignIn page: /signin.html`);
|
||||
|
||||
export default {
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
};
|
||||
@@ -1,176 +0,0 @@
|
||||
import { join } from 'path';
|
||||
|
||||
// PERMANENT: Token Service Module
|
||||
// Singleton module for centralized token acquisition, storage, and refresh
|
||||
// Available to any part of the application via: import { tokenService } from './token-service.ts'
|
||||
//
|
||||
// Token Lifecycle:
|
||||
// - Tokens stored in tokens.json (persistent across restarts)
|
||||
// - Never delete old token before new one acquired
|
||||
// - Background refresh every 5 minutes
|
||||
// - Startup: if PB token exists, resume refresh; else require login
|
||||
//
|
||||
// Usage:
|
||||
// const tokens = await tokenService.getTokens();
|
||||
// await tokenService.saveTokens(pbToken, graphToken);
|
||||
// await tokenService.initialize();
|
||||
// tokenService.startBackgroundRefresh();
|
||||
|
||||
interface TokenData {
|
||||
pbToken: string;
|
||||
pbTokenExpiry: number;
|
||||
graphToken?: string;
|
||||
graphTokenExpiry?: number;
|
||||
lastRefresh: number;
|
||||
}
|
||||
|
||||
const tokensFilePath = join(import.meta.dir, '../../tokens.json');
|
||||
|
||||
class TokenService {
|
||||
private backgroundStarted = false;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Load tokens from file if they exist
|
||||
try {
|
||||
const file = Bun.file(tokensFilePath);
|
||||
if (await file.exists()) {
|
||||
const content = await file.text();
|
||||
const data = JSON.parse(content) as TokenData;
|
||||
console.log('[AuthAndToken] Tokens loaded from file');
|
||||
console.log('[AuthAndToken] PB token valid:', data.pbToken ? 'yes' : 'no');
|
||||
console.log('[AuthAndToken] Graph token valid:', data.graphToken ? 'yes' : 'no');
|
||||
} else {
|
||||
console.log('[AuthAndToken] No tokens.json found - login required on startup');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Token file init error (will create on first login):', (err as Error)?.message);
|
||||
}
|
||||
}
|
||||
|
||||
startBackgroundRefresh(): void {
|
||||
if (this.backgroundStarted) return;
|
||||
this.backgroundStarted = true;
|
||||
|
||||
// PERMANENT: Background token refresh
|
||||
// Runs every 5 minutes, attempts to refresh both PB and Graph tokens
|
||||
// Safe: never deletes old token before new one is confirmed
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await this.refreshTokens();
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Background refresh error:', (err as Error)?.message);
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
|
||||
// Kick off an immediate refresh attempt so we start from a fresh token without waiting
|
||||
this.refreshTokens().catch((err) => {
|
||||
console.log('[AuthAndToken] Immediate refresh error:', (err as Error)?.message);
|
||||
});
|
||||
|
||||
console.log('[AuthAndToken] Background refresh started (every 5 minutes + immediate)');
|
||||
}
|
||||
|
||||
async saveTokens(pbToken: string, graphToken?: string): Promise<void> {
|
||||
// PERMANENT: Token persistence
|
||||
// Called after login or successful refresh
|
||||
// Stores both tokens with expiry times
|
||||
|
||||
const data: TokenData = {
|
||||
pbToken,
|
||||
pbTokenExpiry: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days (PB default)
|
||||
graphToken,
|
||||
graphTokenExpiry: graphToken ? Date.now() + 60 * 60 * 1000 : undefined, // 1 hour (Graph default)
|
||||
lastRefresh: Date.now()
|
||||
};
|
||||
|
||||
await Bun.write(tokensFilePath, JSON.stringify(data, null, 2));
|
||||
console.log('[AuthAndToken] Tokens saved');
|
||||
}
|
||||
|
||||
async getTokens(): Promise<TokenData | null> {
|
||||
try {
|
||||
const file = Bun.file(tokensFilePath);
|
||||
if (!(await file.exists())) return null;
|
||||
|
||||
const content = await file.text();
|
||||
return JSON.parse(content) as TokenData;
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Error reading tokens:', (err as Error)?.message);
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
await fs.unlink(tokensFilePath);
|
||||
console.log('[AuthAndToken] Corrupt tokens file removed');
|
||||
} catch (innerErr) {
|
||||
console.log('[AuthAndToken] Failed to remove corrupt tokens file:', (innerErr as Error)?.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async clearTokens(): Promise<void> {
|
||||
try {
|
||||
const file = Bun.file(tokensFilePath);
|
||||
if (await file.exists()) {
|
||||
const fs = await import('fs/promises');
|
||||
await fs.unlink(tokensFilePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Error clearing tokens:', (err as Error)?.message);
|
||||
}
|
||||
}
|
||||
|
||||
async hasValidPbToken(): Promise<boolean> {
|
||||
// Simplified: treat presence of PB token as valid; no expiry enforcement
|
||||
const tokens = await this.getTokens();
|
||||
return !!tokens?.pbToken;
|
||||
}
|
||||
|
||||
private async refreshTokens(): Promise<void> {
|
||||
// PERMANENT: Auto-refresh logic for both tokens
|
||||
// Called every 5 minutes by background job
|
||||
// Fetches fresh tokens from PocketBase, attempts Graph token refresh
|
||||
// Only updates file after new tokens confirmed valid
|
||||
|
||||
const tokens = await this.getTokens();
|
||||
if (!tokens || !tokens.pbToken) {
|
||||
console.log('[AuthAndToken] No active tokens to refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Refresh via PocketBase endpoint
|
||||
const pbUrl = `${process.env.POCKETBASE_URL}/api/collections/users/auth-refresh`;
|
||||
const response = await fetch(pbUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${tokens.pbToken}`
|
||||
},
|
||||
// @ts-ignore Bun TLS option
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('[AuthAndToken] Refresh failed:', response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json() as any;
|
||||
const newPbToken = data?.token;
|
||||
|
||||
if (!newPbToken) {
|
||||
console.log('[AuthAndToken] No new PB token in response');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tokens with new PB token
|
||||
await this.saveTokens(newPbToken, tokens.graphToken);
|
||||
console.log('[AuthAndToken] Tokens refreshed successfully');
|
||||
} catch (err) {
|
||||
console.log('[AuthAndToken] Refresh error:', (err as Error)?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PERMANENT: Singleton export
|
||||
// Import as: import { tokenService } from './token-service.ts'
|
||||
export const tokenService = new TokenService();
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mode2test",
|
||||
"dependencies": {
|
||||
"dotenv": "17.2.3",
|
||||
"hono": "4.11.4",
|
||||
"pocketbase": "0.26.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "5.9.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 MiB |
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Job Details</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Job Details</h1>
|
||||
<p class="text-gray-600">Details view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,104 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
|
||||
<title>Job Info — Home</title>
|
||||
<link rel="preload" href="assets/CCwhiteApp.png" as="image">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
if (window.tailwind) window.tailwind.config = { corePlugins: { preflight: true } }
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 m-0 text-gray-900 overflow-hidden touch-pan-y h-screen flex flex-col">
|
||||
<div class="max-w-full md:max-w-2xl mx-auto p-4 sm:p-3 text-[17px] flex flex-col flex-1 overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="w-full bg-gray-400 border border-gray-300 rounded-lg px-0.5 py-0.5 mb-3 shadow-sm flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-6 w-[186px] rounded-lg bg-black flex items-center justify-center overflow-hidden">
|
||||
<img src="assets/CCwhiteApp.png" alt="CC" class="h-6 w-auto object-cover scale-80" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0">
|
||||
<a href="https://idea-form.ccllc.pro" target="_blank" rel="noopener" class="h-6 w-12 rounded-3xl overflow-hidden flex items-center justify-center bg-transparent hover:bg-gray-50/20" title="Submit an idea">
|
||||
<img src="assets/lightbulb.jpg" alt="Idea" class="h-full w-auto object-cover" loading="lazy">
|
||||
</a>
|
||||
<a href="https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/ALFONSO/Comprehensive%20Asset%20Inventory.xlsx?d=w9005440917e945d680e9885f33ccf2ba&csf=1&web=1&e=rifTzf" target="_blank" rel="noopener" class="h-6 w-12 rounded-3xl overflow-hidden flex items-center justify-center bg-transparent hover:bg-gray-50/20" title="Open Asset Inventory">
|
||||
<img src="assets/PalmIsland.png" alt="Palm Island" class="h-full w-auto object-cover" loading="lazy">
|
||||
</a>
|
||||
<a href="https://czflex.sharepoint.com/:x:/r/sites/Team/Shared%20Documents/General/ALFONSO/GM%20Rollup.xlsx?d=w3e59f37ee1c04cb798e14f86bd87c968&csf=1&web=1&e=vCnfbR" target="_blank" rel="noopener" class="h-6 w-12 rounded-3xl overflow-hidden flex items-center justify-center bg-transparent hover:bg-gray-50/20" title="Open Operations Dashboard">
|
||||
<img src="assets/DailyCheck.png" alt="Operations Dashboard" class="h-full w-auto object-cover" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="m-0 mb-3 text-center text-blue-600 text-[33px] font-bold">Job Info</h1>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="relative mb-2">
|
||||
<input id="searchBox" placeholder="Search jobs (just start typing or use voice)" class="w-full p-2.5 pr-12 rounded-lg border border-gray-300 text-lg placeholder:text-gray-400">
|
||||
<button id="voiceSearchBtn" class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full flex items-center justify-center hover:bg-gray-100 transition-colors" title="Voice search">
|
||||
<svg id="micIcon" class="w-5 h-5 text-gray-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
|
||||
</svg>
|
||||
<svg id="micRecordingIcon" class="hidden w-5 h-5 text-red-600 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="8"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle -->
|
||||
<button id="filterToggle" class="bg-blue-600 text-white py-2 px-2.5 rounded-lg border-none cursor-pointer mb-2 inline-block min-w-[140px]">Show Filters</button>
|
||||
|
||||
<!-- Filters Panel -->
|
||||
<div id="filtersPanel" class="hidden bg-white border border-gray-200 rounded-lg p-2.5 mb-2.5">
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Division</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="R#"> Residential</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Active</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="No"> No</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Estimator</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Steve Brewer"> Steve</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Beth Cardoza"> Beth</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-sm">
|
||||
<strong>Status</strong><br/>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
|
||||
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
|
||||
</div>
|
||||
<button id="clearFilters" class="bg-gray-500 text-white py-2 px-2 rounded-lg border-none">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" aria-live="polite" class="flex flex-col gap-3 flex-1 overflow-auto p-0"></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-xs text-gray-500 mt-5 pt-3 border-t border-gray-200 flex items-center justify-between">
|
||||
<span id="userEmail" class="truncate"></span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="signOutBtn" class="px-2 py-1 text-[11px] bg-gray-200 hover:bg-gray-300 rounded border border-gray-300 text-gray-700">Sign out</button>
|
||||
<span id="modeLabel" class="px-2 py-1 text-[11px] bg-blue-100 text-blue-700 rounded border border-blue-300">[Mode2Test]</span>
|
||||
<span id="versionLabel">v1.0.0-beta1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/home.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mode2Test</title>
|
||||
<script>
|
||||
// Redirect to appropriate page based on auth status
|
||||
async function checkAuthAndRedirect() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
// Redirect to home
|
||||
window.location.href = '/home.html';
|
||||
} else {
|
||||
// Redirect to signin
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
} catch (err) {
|
||||
// On error, go to signin
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
}
|
||||
|
||||
checkAuthAndRedirect();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="text-align: center; padding: 50px;">
|
||||
<p>Redirecting...</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
} catch {
|
||||
window.location.href = '/signin.html';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Logged in - show app
|
||||
document.getElementById('userName').textContent = pb.authStore.model?.display_name || pb.authStore.model?.email || 'User';
|
||||
document.getElementById('userEmail').textContent = pb.authStore.model?.email || '';
|
||||
|
||||
const response = await fetch('/api/auth/tokens');
|
||||
if (response.ok) {
|
||||
const tokens = await response.json();
|
||||
const pbValid = tokens.pbToken ? '✓ Valid' : '✗ Missing';
|
||||
const graphValid = tokens.graphToken ? '✓ Valid' : '✗ Not acquired';
|
||||
document.getElementById('tokenStatus').innerHTML = `
|
||||
<div><p><strong>PocketBase Token:</strong> ${pbValid}</p></div>
|
||||
<div><p><strong>Graph Token:</strong> ${graphValid}</p></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
if (confirm('Logout?')) {
|
||||
pb.authStore.clear();
|
||||
fetch('/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,344 +0,0 @@
|
||||
// job-cache.js
|
||||
var DB_NAME = "JobInfoCache";
|
||||
var DB_VERSION = 2;
|
||||
var JOBS_STORE = "jobs";
|
||||
var META_STORE = "metadata";
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
var PAGE_SIZE = 500;
|
||||
|
||||
class JobCache {
|
||||
db = null;
|
||||
async initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: "Job_Number" });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
async getCachedJobs() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE], "readonly");
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async isCacheValid() {
|
||||
if (!this.db)
|
||||
return false;
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return false;
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async saveJobs(jobs) {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
jobsStore.clear();
|
||||
jobs.forEach((job) => jobsStore.add(job));
|
||||
const metadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, "main");
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async refreshJobs() {
|
||||
try {
|
||||
let page = 1;
|
||||
let all = [];
|
||||
let totalPages = 1;
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
await this.saveJobs(all);
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh jobs cache:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async getMetadata() {
|
||||
if (!this.db)
|
||||
return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([META_STORE], "readonly");
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get("main");
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async clearCache() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async getCacheAge() {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return null;
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
var jobCache = new JobCache;
|
||||
|
||||
// home.ts
|
||||
var allJobs = [];
|
||||
var filteredJobs = [];
|
||||
var currentPage = 1;
|
||||
var perPage = 50;
|
||||
var isRefreshing = false;
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch("/api/auth/status");
|
||||
const data = await response.json();
|
||||
if (!data.authenticated) {
|
||||
window.location.href = "/signin.html";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Auth check failed:", err);
|
||||
window.location.href = "/signin.html";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function fetchJobs() {
|
||||
try {
|
||||
await jobCache.initialize();
|
||||
const isCacheValid = await jobCache.isCacheValid();
|
||||
if (isCacheValid) {
|
||||
const cachedJobs = await jobCache.getCachedJobs();
|
||||
allJobs = cachedJobs;
|
||||
filteredJobs = [...allJobs];
|
||||
renderJobs();
|
||||
const cacheAge = await jobCache.getCacheAge();
|
||||
console.log(`Loaded ${allJobs.length} jobs from cache (age: ${Math.round((cacheAge || 0) / 1000)}s)`);
|
||||
refreshJobsInBackground();
|
||||
} else {
|
||||
const jobs = await jobCache.refreshJobs();
|
||||
allJobs = jobs;
|
||||
filteredJobs = [...allJobs];
|
||||
renderJobs();
|
||||
console.log(`Loaded ${allJobs.length} jobs from server (cache updated)`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch jobs:", err);
|
||||
const results = document.getElementById("results");
|
||||
results.innerHTML = '<div class="text-center text-red-600 p-4">Failed to load jobs. Please refresh the page.</div>';
|
||||
}
|
||||
}
|
||||
async function refreshJobsInBackground() {
|
||||
if (isRefreshing)
|
||||
return;
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const freshJobs = await jobCache.refreshJobs();
|
||||
allJobs = freshJobs;
|
||||
applyFilters();
|
||||
console.log(`Background refresh complete (${freshJobs.length} jobs)`);
|
||||
} catch (err) {
|
||||
console.error("Background refresh failed:", err);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
function renderJobs() {
|
||||
const results = document.getElementById("results");
|
||||
if (filteredJobs.length === 0) {
|
||||
results.innerHTML = '<div class="text-center text-gray-500 p-4">No jobs found</div>';
|
||||
return;
|
||||
}
|
||||
const start = (currentPage - 1) * perPage;
|
||||
const end = start + perPage;
|
||||
const pageJobs = filteredJobs.slice(start, end);
|
||||
results.innerHTML = pageJobs.map((job) => {
|
||||
const isActiveTrue = job.Active === true;
|
||||
const isActiveFalse = job.Active === false;
|
||||
const nameVal = job.Job_Full_Name || "";
|
||||
const contactVal = job.Contact_Person || "";
|
||||
return `
|
||||
<div class="bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1 hover:bg-gray-50 transition-colors" data-job-number="${escapeHtml(String(job.Job_Number || ""))}">
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(job.Job_Number || ""))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(nameVal))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(contactVal))}</div></div>
|
||||
<div class="absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm" style="background:${isActiveTrue ? "green" : isActiveFalse ? "red" : "#cbd5e1"}"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
results.querySelectorAll("[data-job-number]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
const jobNumber = e.currentTarget.dataset.jobNumber;
|
||||
if (jobNumber) {
|
||||
openDetails(jobNumber);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function openDetails(jobNumber) {
|
||||
window.location.href = `/details.html?job=${encodeURIComponent(jobNumber)}`;
|
||||
}
|
||||
function setupSearch() {
|
||||
const searchBox = document.getElementById("searchBox");
|
||||
searchBox.addEventListener("input", () => {
|
||||
const query = searchBox.value.toLowerCase().trim();
|
||||
if (!query) {
|
||||
filteredJobs = [...allJobs];
|
||||
} else {
|
||||
filteredJobs = allJobs.filter((job) => {
|
||||
return job.Job_Number?.toString().toLowerCase().includes(query) || job.Job_Full_Name?.toLowerCase().includes(query) || job.Job_Address?.toLowerCase().includes(query) || job.Contact_Person?.toLowerCase().includes(query) || job.Company_Client?.toLowerCase().includes(query) || job.Notes?.toLowerCase().includes(query) || job.Job_Status?.toLowerCase().includes(query) || job.Status?.toLowerCase().includes(query) || job.Estimator?.toLowerCase().includes(query);
|
||||
});
|
||||
}
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
});
|
||||
}
|
||||
function setupFilters() {
|
||||
const filterToggle = document.getElementById("filterToggle");
|
||||
const filtersPanel = document.getElementById("filtersPanel");
|
||||
const clearFilters = document.getElementById("clearFilters");
|
||||
filterToggle.addEventListener("click", () => {
|
||||
const isHidden = filtersPanel.classList.contains("hidden");
|
||||
if (isHidden) {
|
||||
filtersPanel.classList.remove("hidden");
|
||||
filterToggle.textContent = "Hide Filters";
|
||||
} else {
|
||||
filtersPanel.classList.add("hidden");
|
||||
filterToggle.textContent = "Show Filters";
|
||||
}
|
||||
});
|
||||
const filterInputs = filtersPanel.querySelectorAll('input[type="checkbox"]');
|
||||
filterInputs.forEach((input) => {
|
||||
input.addEventListener("change", applyFilters);
|
||||
});
|
||||
clearFilters.addEventListener("click", () => {
|
||||
filterInputs.forEach((input) => {
|
||||
input.checked = false;
|
||||
});
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
function applyFilters() {
|
||||
const divisions = Array.from(document.querySelectorAll(".filter-division:checked")).map((el) => el.value);
|
||||
const actives = Array.from(document.querySelectorAll(".filter-active:checked")).map((el) => el.value);
|
||||
const estimators = Array.from(document.querySelectorAll(".filter-estimator:checked")).map((el) => el.value.toLowerCase());
|
||||
const statuses = Array.from(document.querySelectorAll(".filter-status:checked")).map((el) => el.value);
|
||||
filteredJobs = allJobs.filter((job) => {
|
||||
const divisionVal = job.Job_Division || job.Division;
|
||||
const activeVal = job.Active === true || job.Active === "Yes" ? "Yes" : job.Active === false || job.Active === "No" ? "No" : "—";
|
||||
const estimatorVal = (job.Estimator || "").toLowerCase();
|
||||
const statusVal = job.Job_Status || job.Status;
|
||||
if (divisions.length > 0 && !divisions.some((d) => divisionVal?.startsWith(d)))
|
||||
return false;
|
||||
if (actives.length > 0 && !actives.includes(activeVal))
|
||||
return false;
|
||||
if (estimators.length > 0 && !estimators.some((e) => estimatorVal.includes(e)))
|
||||
return false;
|
||||
if (statuses.length > 0 && !statuses.includes(statusVal || ""))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
}
|
||||
function setupVoiceSearch() {
|
||||
const voiceBtn = document.getElementById("voiceSearchBtn");
|
||||
const micIcon = document.getElementById("micIcon");
|
||||
const micRecordingIcon = document.getElementById("micRecordingIcon");
|
||||
const searchBox = document.getElementById("searchBox");
|
||||
if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) {
|
||||
voiceBtn.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
const recognition = new SpeechRecognition;
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
voiceBtn.addEventListener("click", () => {
|
||||
recognition.start();
|
||||
micIcon.classList.add("hidden");
|
||||
micRecordingIcon.classList.remove("hidden");
|
||||
});
|
||||
recognition.onresult = (event) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
searchBox.value = transcript;
|
||||
searchBox.dispatchEvent(new Event("input"));
|
||||
};
|
||||
recognition.onend = () => {
|
||||
micIcon.classList.remove("hidden");
|
||||
micRecordingIcon.classList.add("hidden");
|
||||
};
|
||||
recognition.onerror = () => {
|
||||
micIcon.classList.remove("hidden");
|
||||
micRecordingIcon.classList.add("hidden");
|
||||
};
|
||||
}
|
||||
function setupSignOut() {
|
||||
const signOutBtn = document.getElementById("signOutBtn");
|
||||
signOutBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
window.location.href = "/signin.html";
|
||||
} catch (err) {
|
||||
console.error("Sign out failed:", err);
|
||||
window.location.href = "/signin.html";
|
||||
}
|
||||
});
|
||||
}
|
||||
async function init() {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (!isAuthenticated)
|
||||
return;
|
||||
setupSearch();
|
||||
setupFilters();
|
||||
setupVoiceSearch();
|
||||
setupSignOut();
|
||||
await fetchJobs();
|
||||
}
|
||||
function escapeHtml(str) {
|
||||
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
||||
}
|
||||
init();
|
||||
@@ -1,330 +0,0 @@
|
||||
// PERMANENT: Home view - Jobs listing with zero-delay loading via IndexedDB cache
|
||||
// Wired to tokenService via backend API routes
|
||||
// Visual match to Mode1Test but uses new architecture
|
||||
//
|
||||
// Dependencies:
|
||||
// - Backend /api/auth/status for authentication check
|
||||
// - Backend /api/jobs for job data
|
||||
// - job-cache.ts for IndexedDB caching
|
||||
//
|
||||
// Features:
|
||||
// - Zero-delay load (displays cached jobs instantly)
|
||||
// - Background refresh after displaying cache
|
||||
// - Search with voice support (operates on cached data)
|
||||
// - Filters (division, active, estimator, status)
|
||||
// - Pagination
|
||||
// - Click job to open Details view
|
||||
|
||||
import { jobCache, type JobCard } from './job-cache.js';
|
||||
|
||||
interface Job {
|
||||
Job_Number: string;
|
||||
Job_Name?: string;
|
||||
Job_Full_Name?: string;
|
||||
Job_Address?: string;
|
||||
Job_Division?: string;
|
||||
Active?: boolean | string;
|
||||
Status?: string;
|
||||
Job_Status?: string;
|
||||
Estimator?: string;
|
||||
Contact_Person?: string;
|
||||
Company_Client?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let allJobs: Job[] = [];
|
||||
let filteredJobs: Job[] = [];
|
||||
let currentPage = 1;
|
||||
const perPage = 50;
|
||||
let isRefreshing = false;
|
||||
|
||||
// Check authentication on page load
|
||||
async function checkAuth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.authenticated) {
|
||||
// Redirect to login
|
||||
window.location.href = '/signin.html';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
window.location.href = '/signin.html';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch jobs from backend (or cache)
|
||||
async function fetchJobs(): Promise<void> {
|
||||
try {
|
||||
// Initialize cache
|
||||
await jobCache.initialize();
|
||||
|
||||
// Check if cache is valid
|
||||
const isCacheValid = await jobCache.isCacheValid();
|
||||
|
||||
if (isCacheValid) {
|
||||
// Display cached jobs immediately (zero delay)
|
||||
const cachedJobs = await jobCache.getCachedJobs();
|
||||
allJobs = cachedJobs;
|
||||
filteredJobs = [...allJobs];
|
||||
renderJobs();
|
||||
|
||||
// Show cache age in console for debugging
|
||||
const cacheAge = await jobCache.getCacheAge();
|
||||
console.log(`Loaded ${allJobs.length} jobs from cache (age: ${Math.round((cacheAge || 0) / 1000)}s)`);
|
||||
|
||||
// Refresh in background (don't block UI)
|
||||
refreshJobsInBackground();
|
||||
} else {
|
||||
// No valid cache, fetch fresh data (no progress bar)
|
||||
const jobs = await jobCache.refreshJobs();
|
||||
allJobs = jobs;
|
||||
filteredJobs = [...allJobs];
|
||||
|
||||
renderJobs();
|
||||
console.log(`Loaded ${allJobs.length} jobs from server (cache updated)`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch jobs:', err);
|
||||
|
||||
// Show error message
|
||||
const results = document.getElementById('results') as HTMLDivElement;
|
||||
results.innerHTML = '<div class="text-center text-red-600 p-4">Failed to load jobs. Please refresh the page.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Background refresh (silent, doesn't interrupt user)
|
||||
async function refreshJobsInBackground(): Promise<void> {
|
||||
if (isRefreshing) return;
|
||||
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const freshJobs = await jobCache.refreshJobs();
|
||||
|
||||
// Update in memory without interrupting user
|
||||
allJobs = freshJobs;
|
||||
|
||||
// Reapply current filters and search
|
||||
applyFilters();
|
||||
|
||||
console.log(`Background refresh complete (${freshJobs.length} jobs)`);
|
||||
} catch (err) {
|
||||
console.error('Background refresh failed:', err);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Render jobs to DOM
|
||||
function renderJobs(): void {
|
||||
const results = document.getElementById('results') as HTMLDivElement;
|
||||
|
||||
if (filteredJobs.length === 0) {
|
||||
results.innerHTML = '<div class="text-center text-gray-500 p-4">No jobs found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const start = (currentPage - 1) * perPage;
|
||||
const end = start + perPage;
|
||||
const pageJobs = filteredJobs.slice(start, end);
|
||||
|
||||
results.innerHTML = pageJobs.map(job => {
|
||||
const isActiveTrue = job.Active === true;
|
||||
const isActiveFalse = job.Active === false;
|
||||
const nameVal = job.Job_Full_Name || '';
|
||||
const contactVal = job.Contact_Person || '';
|
||||
|
||||
return `
|
||||
<div class="bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1 hover:bg-gray-50 transition-colors" data-job-number="${escapeHtml(String(job.Job_Number||''))}">
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(job.Job_Number||''))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(nameVal))}</div></div>
|
||||
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(contactVal))}</div></div>
|
||||
<div class="absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm" style="background:${isActiveTrue?'green':(isActiveFalse?'red':'#cbd5e1')}"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add click handlers
|
||||
results.querySelectorAll('[data-job-number]').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const jobNumber = (e.currentTarget as HTMLElement).dataset.jobNumber;
|
||||
if (jobNumber) {
|
||||
openDetails(jobNumber);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Open Details view
|
||||
function openDetails(jobNumber: string): void {
|
||||
// Navigate to details view with job number
|
||||
window.location.href = `/details.html?job=${encodeURIComponent(jobNumber)}`;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function setupSearch(): void {
|
||||
const searchBox = document.getElementById('searchBox') as HTMLInputElement;
|
||||
|
||||
searchBox.addEventListener('input', () => {
|
||||
const query = searchBox.value.toLowerCase().trim();
|
||||
|
||||
if (!query) {
|
||||
filteredJobs = [...allJobs];
|
||||
} else {
|
||||
filteredJobs = allJobs.filter(job => {
|
||||
return (
|
||||
job.Job_Number?.toString().toLowerCase().includes(query) ||
|
||||
job.Job_Full_Name?.toLowerCase().includes(query) ||
|
||||
job.Job_Address?.toLowerCase().includes(query) ||
|
||||
job.Contact_Person?.toLowerCase().includes(query) ||
|
||||
job.Company_Client?.toLowerCase().includes(query) ||
|
||||
job.Notes?.toLowerCase().includes(query) ||
|
||||
job.Job_Status?.toLowerCase().includes(query) ||
|
||||
job.Status?.toLowerCase().includes(query) ||
|
||||
job.Estimator?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
});
|
||||
}
|
||||
|
||||
// Filter functionality
|
||||
function setupFilters(): void {
|
||||
const filterToggle = document.getElementById('filterToggle') as HTMLButtonElement;
|
||||
const filtersPanel = document.getElementById('filtersPanel') as HTMLDivElement;
|
||||
const clearFilters = document.getElementById('clearFilters') as HTMLButtonElement;
|
||||
|
||||
filterToggle.addEventListener('click', () => {
|
||||
const isHidden = filtersPanel.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
filtersPanel.classList.remove('hidden');
|
||||
filterToggle.textContent = 'Hide Filters';
|
||||
} else {
|
||||
filtersPanel.classList.add('hidden');
|
||||
filterToggle.textContent = 'Show Filters';
|
||||
}
|
||||
});
|
||||
|
||||
// Apply filters on change
|
||||
const filterInputs = filtersPanel.querySelectorAll('input[type="checkbox"]');
|
||||
filterInputs.forEach(input => {
|
||||
input.addEventListener('change', applyFilters);
|
||||
});
|
||||
|
||||
clearFilters.addEventListener('click', () => {
|
||||
filterInputs.forEach(input => {
|
||||
(input as HTMLInputElement).checked = false;
|
||||
});
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters(): void {
|
||||
const divisions = Array.from(document.querySelectorAll('.filter-division:checked')).map(el => (el as HTMLInputElement).value);
|
||||
const actives = Array.from(document.querySelectorAll('.filter-active:checked')).map(el => (el as HTMLInputElement).value);
|
||||
const estimators = Array.from(document.querySelectorAll('.filter-estimator:checked')).map(el => (el as HTMLInputElement).value.toLowerCase());
|
||||
const statuses = Array.from(document.querySelectorAll('.filter-status:checked')).map(el => (el as HTMLInputElement).value);
|
||||
|
||||
filteredJobs = allJobs.filter(job => {
|
||||
const divisionVal = job.Job_Division || job.Division;
|
||||
const activeVal = job.Active === true || job.Active === 'Yes' ? 'Yes' : job.Active === false || job.Active === 'No' ? 'No' : '—';
|
||||
const estimatorVal = (job.Estimator || '').toLowerCase();
|
||||
const statusVal = job.Job_Status || job.Status;
|
||||
|
||||
if (divisions.length > 0 && !divisions.some(d => divisionVal?.startsWith(d))) return false;
|
||||
if (actives.length > 0 && !actives.includes(activeVal)) return false;
|
||||
if (estimators.length > 0 && !estimators.some(e => estimatorVal.includes(e))) return false;
|
||||
if (statuses.length > 0 && !statuses.includes(statusVal || '')) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
renderJobs();
|
||||
}
|
||||
|
||||
// Voice search
|
||||
function setupVoiceSearch(): void {
|
||||
const voiceBtn = document.getElementById('voiceSearchBtn') as HTMLButtonElement;
|
||||
const micIcon = document.getElementById('micIcon') as SVGElement;
|
||||
const micRecordingIcon = document.getElementById('micRecordingIcon') as SVGElement;
|
||||
const searchBox = document.getElementById('searchBox') as HTMLInputElement;
|
||||
|
||||
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
||||
voiceBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
||||
const recognition = new SpeechRecognition();
|
||||
recognition.continuous = false;
|
||||
recognition.interimResults = false;
|
||||
|
||||
voiceBtn.addEventListener('click', () => {
|
||||
recognition.start();
|
||||
micIcon.classList.add('hidden');
|
||||
micRecordingIcon.classList.remove('hidden');
|
||||
});
|
||||
|
||||
recognition.onresult = (event: any) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
searchBox.value = transcript;
|
||||
searchBox.dispatchEvent(new Event('input'));
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
micIcon.classList.remove('hidden');
|
||||
micRecordingIcon.classList.add('hidden');
|
||||
};
|
||||
|
||||
recognition.onerror = () => {
|
||||
micIcon.classList.remove('hidden');
|
||||
micRecordingIcon.classList.add('hidden');
|
||||
};
|
||||
}
|
||||
|
||||
// Sign out
|
||||
function setupSignOut(): void {
|
||||
const signOutBtn = document.getElementById('signOutBtn') as HTMLButtonElement;
|
||||
|
||||
signOutBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/signin.html';
|
||||
} catch (err) {
|
||||
console.error('Sign out failed:', err);
|
||||
window.location.href = '/signin.html';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
async function init(): Promise<void> {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
setupSearch();
|
||||
setupFilters();
|
||||
setupVoiceSearch();
|
||||
setupSignOut();
|
||||
|
||||
await fetchJobs();
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
||||
|
||||
// Start
|
||||
init();
|
||||
@@ -1,129 +0,0 @@
|
||||
// job-cache.ts
|
||||
var DB_NAME = "JobInfoCache";
|
||||
var DB_VERSION = 2;
|
||||
var JOBS_STORE = "jobs";
|
||||
var META_STORE = "metadata";
|
||||
var CACHE_TTL = 5 * 60 * 1000;
|
||||
var PAGE_SIZE = 500;
|
||||
|
||||
class JobCache {
|
||||
db = null;
|
||||
async initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: "Job_Number" });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
async getCachedJobs() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE], "readonly");
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async isCacheValid() {
|
||||
if (!this.db)
|
||||
return false;
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return false;
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async saveJobs(jobs) {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
jobsStore.clear();
|
||||
jobs.forEach((job) => jobsStore.add(job));
|
||||
const metadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, "main");
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async refreshJobs() {
|
||||
try {
|
||||
let page = 1;
|
||||
let all = [];
|
||||
let totalPages = 1;
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const items = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
await this.saveJobs(all);
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh jobs cache:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
async getMetadata() {
|
||||
if (!this.db)
|
||||
return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([META_STORE], "readonly");
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get("main");
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
async clearCache() {
|
||||
if (!this.db)
|
||||
throw new Error("Database not initialized");
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite");
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
async getCacheAge() {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated)
|
||||
return null;
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
var jobCache = new JobCache;
|
||||
export {
|
||||
jobCache
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
// PERMANENT: IndexedDB cache for jobs data
|
||||
// Provides lightning-fast job listing with zero-delay loading
|
||||
//
|
||||
// Strategy:
|
||||
// - Display cached jobs instantly on page load (0ms delay)
|
||||
// - Fetch fresh data in background after displaying cache
|
||||
// - Cache TTL: 5 minutes (configurable)
|
||||
// - Stores minimal fields for card display only
|
||||
//
|
||||
// Fields cached: Job_Number, Job_Name, Division, Active, Status, Estimator
|
||||
//
|
||||
// Usage:
|
||||
// await jobCache.initialize();
|
||||
// const jobs = await jobCache.getCachedJobs(); // Instant
|
||||
// await jobCache.refreshJobs(); // Background refresh
|
||||
|
||||
export interface JobCard {
|
||||
Job_Number: string;
|
||||
Job_Name: string;
|
||||
Division: string;
|
||||
Active: string;
|
||||
Status: string;
|
||||
Estimator: string;
|
||||
}
|
||||
|
||||
interface CacheMetadata {
|
||||
lastUpdated: number;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const DB_NAME = 'JobInfoCache';
|
||||
const DB_VERSION = 2; // bump to invalidate old minimal-field cache
|
||||
const JOBS_STORE = 'jobs';
|
||||
const META_STORE = 'metadata';
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
const PAGE_SIZE = 500; // match Mode1 bulk fetch
|
||||
|
||||
class JobCache {
|
||||
private db: IDBDatabase | null = null;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Create jobs store with Job_Number as key
|
||||
if (!db.objectStoreNames.contains(JOBS_STORE)) {
|
||||
db.createObjectStore(JOBS_STORE, { keyPath: 'Job_Number' });
|
||||
}
|
||||
|
||||
// Create metadata store
|
||||
if (!db.objectStoreNames.contains(META_STORE)) {
|
||||
db.createObjectStore(META_STORE);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getCachedJobs(): Promise<JobCard[]> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(JOBS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async isCacheValid(): Promise<boolean> {
|
||||
if (!this.db) return false;
|
||||
|
||||
try {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated) return false;
|
||||
|
||||
const age = Date.now() - metadata.lastUpdated;
|
||||
return age < CACHE_TTL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveJobs(jobs: JobCard[]): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite');
|
||||
const jobsStore = transaction.objectStore(JOBS_STORE);
|
||||
const metaStore = transaction.objectStore(META_STORE);
|
||||
|
||||
// Clear existing jobs
|
||||
jobsStore.clear();
|
||||
|
||||
// Save new jobs
|
||||
jobs.forEach(job => jobsStore.add(job));
|
||||
|
||||
// Update metadata
|
||||
const metadata: CacheMetadata = {
|
||||
lastUpdated: Date.now(),
|
||||
version: DB_VERSION
|
||||
};
|
||||
metaStore.put(metadata, 'main');
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async refreshJobs(): Promise<JobCard[]> {
|
||||
try {
|
||||
// Fetch fresh data from backend with pagination to mirror Mode1 behavior
|
||||
let page = 1;
|
||||
let all: JobCard[] = [];
|
||||
let totalPages = 1;
|
||||
|
||||
while (page <= totalPages) {
|
||||
const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch jobs (status ${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const items: JobCard[] = data.items || [];
|
||||
const reportedTotalPages = data.totalPages || 1;
|
||||
|
||||
all = all.concat(items);
|
||||
totalPages = Math.max(totalPages, reportedTotalPages);
|
||||
page += 1;
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
await this.saveJobs(all);
|
||||
|
||||
return all;
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh jobs cache:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetadata(): Promise<CacheMetadata | null> {
|
||||
if (!this.db) return null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([META_STORE], 'readonly');
|
||||
const store = transaction.objectStore(META_STORE);
|
||||
const request = store.get('main');
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([JOBS_STORE, META_STORE], 'readwrite');
|
||||
|
||||
transaction.objectStore(JOBS_STORE).clear();
|
||||
transaction.objectStore(META_STORE).clear();
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getCacheAge(): Promise<number | null> {
|
||||
const metadata = await this.getMetadata();
|
||||
if (!metadata || !metadata.lastUpdated) return null;
|
||||
|
||||
return Date.now() - metadata.lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobCache = new JobCache();
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Manager</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Manager</h1>
|
||||
<p class="text-gray-600">Manager view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Navigator</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Navigator</h1>
|
||||
<p class="text-gray-600">Navigator view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,87 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In - Mode2Test</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gradient-to-br from-blue-500 to-indigo-600 min-h-screen flex items-center justify-center">
|
||||
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
|
||||
<h1 class="text-gray-800 mb-2 text-3xl font-bold">Mode2Test</h1>
|
||||
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
|
||||
|
||||
<button id="loginBtn" onclick="handleLogin()" class="w-full bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400">
|
||||
Sign in with Microsoft
|
||||
</button>
|
||||
|
||||
<div id="error" class="text-red-600 mt-4 hidden"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
async function handleLogin() {
|
||||
const btn = document.getElementById('loginBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Signing in...';
|
||||
document.getElementById('error').classList.add('hidden');
|
||||
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
|
||||
|
||||
// Log what we got for debugging
|
||||
console.log('[Auth] OAuth response:', authData);
|
||||
console.log('[Auth] Meta:', authData?.meta);
|
||||
|
||||
// Try to extract graph token from various possible locations
|
||||
const graphToken =
|
||||
authData?.meta?.accessToken ||
|
||||
authData?.meta?.access_token ||
|
||||
authData?.meta?.graphAccessToken ||
|
||||
authData?.meta?.graph_token ||
|
||||
authData?.meta?.graphToken ||
|
||||
authData?.meta?.rawUser?.access_token ||
|
||||
'';
|
||||
|
||||
console.log('[Auth] Graph token found:', !!graphToken);
|
||||
|
||||
// Send to backend to store
|
||||
const saveResponse = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pbToken: authData.token, graphToken: graphToken || undefined })
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error('Failed to save tokens to backend');
|
||||
}
|
||||
|
||||
console.log('[Auth] Tokens saved to backend, redirecting to home');
|
||||
|
||||
// Clear PocketBase local storage to avoid conflicts
|
||||
pb.authStore.clear();
|
||||
|
||||
// Redirect to home page
|
||||
window.location.href = '/home.html';
|
||||
} catch (err) {
|
||||
document.getElementById('error').textContent = err.message || 'Login failed';
|
||||
document.getElementById('error').classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Sign in with Microsoft';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already logged in via backend only
|
||||
fetch('/api/auth/status')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.authenticated) {
|
||||
window.location.href = '/home.html';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Viewer</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 p-4">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-4 text-blue-600">Viewer</h1>
|
||||
<p class="text-gray-600">Viewer view - Under construction</p>
|
||||
<a href="/home.html" class="text-blue-600 underline mt-4 inline-block">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "mode2test",
|
||||
"version": "1.0.0-beta1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run backend/server.ts",
|
||||
"start": "bun run backend/server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "4.11.4",
|
||||
"dotenv": "17.2.3",
|
||||
"pocketbase": "0.26.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"pbToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE4MDAyMjIzMjgsImlkIjoiMWhiaWY5ZGJxbmc4Nmg5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.lkHN__Eh6ECrVtlQmCjl7sVZ1B2KMg75FnRjUNzsUtU",
|
||||
"pbTokenExpiry": 1769291128701,
|
||||
"graphToken": "eyJ0eXAiOiJKV1QiLCJub25jZSI6InRSOVQyM1h4RXhKRHN1YXJuSkdmcmYzd2FDS094TF9xWUkwd2dLUjhMVTQiLCJhbGciOiJSUzI1NiIsIng1dCI6IlBjWDk4R1g0MjBUMVg2c0JEa3poUW1xZ3dNVSIsImtpZCI6IlBjWDk4R1g0MjBUMVg2c0JEa3poUW1xZ3dNVSJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8zZmQ5N2VhNy1iMTI0LTQxZjEtODU1Zi01MmQ4YWMzYjE2YzcvIiwiaWF0IjoxNzY4Njc4Mjk1LCJuYmYiOjE3Njg2NzgyOTUsImV4cCI6MTc2ODY4MjIxMSwiYWNjdCI6MCwiYWNyIjoiMSIsImFjcnMiOlsicDEiXSwiYWlvIjoiQWRRQUsvOGJBQUFBMW1BM04rN1pMMFU5OURDcUtsZ2JiOE1pSlhYRXZOT0dMQ2FjVllmdWZMTVM3b2N1emlvdkVmd1dZQnRBV2VyK0U5bGZxUU9FZ1lVZnBOODAxK2srMXBqOW5NRHlqdHhuRWVkaWI5Q1hZbThTMmFLeVNmZWFaQTFFZVdOa2VqWkduN2ZRQSsxLzE5ckVKcWlqMlRRRWZBMm9RTTlDamwxSE5QbGpGQ05iTHZwcmtETFIrT29CVlM4VlBXZi8zTU00Q1pzQ2ZiZnNRb3FuSGpzUHBUTzdFMzk1Qk5UQVhveXoxL1lRTkFucVhmNHRGajVaMUZVWW9jY1hES0NubEc0NXBmTVMxSHMreE1ZWVdnT3h0MkhJeVE9PSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwX2Rpc3BsYXluYW1lIjoiSm9iX0luZm9fU3luYyIsImFwcGlkIjoiM2M4NDZlNzEtOTYwOS00MGUxLWI0NTgtMGViODA1ZTIxYjlmIiwiYXBwaWRhY3IiOiIxIiwiZGV2aWNlaWQiOiJkZjdmZmUyOC04NDQ2LTQ5NjUtOWNlMC1jNDJiNjIxZmFiYjciLCJmYW1pbHlfbmFtZSI6IkV3aW5nIiwiZ2l2ZW5fbmFtZSI6IkFsZm9uc28iLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIxNDMuMTA1LjI0Ny4xNDYiLCJuYW1lIjoiQWxmb25zbyBFd2luZyIsIm9pZCI6IjFhNmU5ZDEwLTEzOGEtNDNlNC1hMDllLTFmNTA0NjMxNDhjOSIsInBsYXRmIjoiMyIsInB1aWQiOiIxMDAzMjAwNTFGRUI4QUMwIiwicmgiOiIxLkFUVUFwMzdaUHlTeDhVR0ZYMUxZckRzV3h3TUFBQUFBQUFBQXdBQUFBQUFBQUFCbEFVazFBQS4iLCJzY3AiOiJDaGFubmVsLlJlYWRCYXNpYy5BbGwgQ2hhbm5lbE1lbWJlci5SZWFkLkFsbCBDaGFubmVsTWVtYmVyLlJlYWRXcml0ZS5BbGwgQ2hhbm5lbE1lc3NhZ2UuRWRpdCBDaGFubmVsTWVzc2FnZS5SZWFkLkFsbCBDaGFubmVsTWVzc2FnZS5SZWFkV3JpdGUgQ2hhbm5lbE1lc3NhZ2UuU2VuZCBDaGFubmVsU2V0dGluZ3MuUmVhZC5BbGwgQ2hhbm5lbFNldHRpbmdzLlJlYWRXcml0ZS5BbGwgQ2hhdE1lc3NhZ2UuUmVhZCBDaGF0TWVzc2FnZS5TZW5kIERpcmVjdG9yeS5BY2Nlc3NBc1VzZXIuQWxsIERpcmVjdG9yeS5SZWFkLkFsbCBEaXJlY3RvcnkuUmVhZFdyaXRlLkFsbCBlbWFpbCBGaWxlcy5SZWFkV3JpdGUuQWxsIEdyb3VwLlJlYWQuQWxsIEdyb3VwLlJlYWRXcml0ZS5BbGwgR3JvdXAtQ29udmVyc2F0aW9uLlJlYWQuQWxsIEdyb3VwLUNvbnZlcnNhdGlvbi5SZWFkV3JpdGUuQWxsIEdyb3VwTWVtYmVyLlJlYWQuQWxsIEdyb3VwTWVtYmVyLlJlYWRXcml0ZS5BbGwgR3JvdXBTZXR0aW5ncy5SZWFkLkFsbCBHcm91cFNldHRpbmdzLlJlYWRXcml0ZS5BbGwgcHJvZmlsZSBTaXRlcy5SZWFkV3JpdGUuQWxsIFRhc2tzLlJlYWQgVGFza3MuUmVhZC5TaGFyZWQgVGFza3MuUmVhZFdyaXRlIFRhc2tzLlJlYWRXcml0ZS5TaGFyZWQgVGVhbXNBY3Rpdml0eS5SZWFkIFRlYW1TZXR0aW5ncy5SZWFkLkFsbCBUZWFtU2V0dGluZ3MuUmVhZFdyaXRlLkFsbCBUZWFtc1BvbGljeVVzZXJBc3NpZ24uUmVhZFdyaXRlLkFsbCBVc2VyLlJlYWQgb3BlbmlkIiwic2lkIjoiMDA4ZDJjYjktODU5MS1jMDdkLTVhMjQtYzk2MjhkYzc1ZDIzIiwic2lnbmluX3N0YXRlIjpbImttc2kiXSwic3ViIjoiQXNWc0d6U3NUTmFWblJiTWxDWnlWeWtZMlh6NEhyS0RpRWx1eUVaWXZTdyIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJOQSIsInRpZCI6IjNmZDk3ZWE3LWIxMjQtNDFmMS04NTVmLTUyZDhhYzNiMTZjNyIsInVuaXF1ZV9uYW1lIjoiYWV3aW5nQGNhcmRvemEuY29uc3RydWN0aW9uIiwidXBuIjoiYWV3aW5nQGNhcmRvemEuY29uc3RydWN0aW9uIiwidXRpIjoiVDljb0FWV3ZXVTJTbTBGLUFqS3lBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiZmU5MzBiZTctNWU2Mi00N2RiLTkxYWYtOThjM2E0OWEzOGIxIiwiNjJlOTAzOTQtNjlmNS00MjM3LTkxOTAtMDEyMTc3MTQ1ZTEwIiwiMjkyMzJjZGYtOTMyMy00MmZkLWFkZTItMWQwOTdhZjNlNGRlIiwiNjkwOTEyNDYtMjBlOC00YTU2LWFhNGQtMDY2MDc1YjJhN2E4IiwiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19hY2QiOjE3NjM0MDU4NjQsInhtc19hY3RfZmN0IjoiOSAzIiwieG1zX2Z0ZCI6IkYyUXJpNWN3ZEhLQzVuZzFrYjdsU1huX3BlWXFaR3JUT21KeDYyN21LZUVCZFhOM1pYTjBNeTFrYzIxeiIsInhtc19pZHJlbCI6IjggMSIsInhtc19zdCI6eyJzdWIiOiJBSGY2UURxRXRkdkR5em1aMHlMMnRmZEktSW1ZUnFGVElRYUxzN2JKdlBjIn0sInhtc19zdWJfZmN0IjoiMyA2IiwieG1zX3RjZHQiOjE1NTg1NDYwMDcsInhtc190bnRfZmN0IjoiNiAzIn0.UqBTFe_GkrUqp9oHgjO93KtXckuDH91RQ03fV8ZzgUit4Vre1EXctVC-Zm5j5kfSM_6pb99VKasBPAolbIa4U-_J1yGidc81sghJj3Haz048v9_074Ky4Qzl0czNQ7OYTn2N1xKuYeBEzXCMt6Cc8gpGJv372seP0CVTs6ZxFTAfRXw2PvHsrJdiEJQMpMoSLBRC_bF9s223o6PiN3v1XgGbAqwP3WI-0GhtyK5Tnul0WJIH5i9G2sA3DlDAmxFm51RwxZtaeqhYPGNEkHrUQqBkNHeAiZ_VqvmGHyuj-D7E-XHOGkF8bQPCHWVXeLdZJNkOxxBEgqYNBpp2QWBWVg",
|
||||
"graphTokenExpiry": 1768689928701,
|
||||
"lastRefresh": 1768686328701
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Mode3 - QuickBooks SOAP Integration
|
||||
|
||||
This mode provides a SOAP server for QuickBooks desktop integration via the QuickBooks Web Connector.
|
||||
|
||||
## Features
|
||||
|
||||
- SOAP endpoint for QuickBooks Web Connector
|
||||
- Query and update QuickBooks entities (customers, invoices, etc.)
|
||||
- Request authentication and session management
|
||||
- Change detection and queue management
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd Mode3
|
||||
bun install
|
||||
bun run backend/server.ts
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file with:
|
||||
|
||||
```
|
||||
QUICKBOOKS_USER_NAME=your_username
|
||||
QUICKBOOKS_PASSWORD=your_password
|
||||
QUICKBOOKS_FILE_PATH=/path/to/quickbooks/file
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `POST /ws` - SOAP endpoint for QuickBooks Web Connector
|
||||
- `POST /quickbooks/auth` - Authenticate with QuickBooks
|
||||
- `GET /quickbooks/status` - Get current QB session status
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "job-info",
|
||||
"name": "job-info-mode3",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"hono": "^4.10.8",
|
||||
"ioredis": "^5.8.2",
|
||||
"pocketbase": "^0.26.5",
|
||||
"soap": "^0.12.0",
|
||||
"tailwind": "^4.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
@@ -25,13 +25,9 @@
|
||||
"packages": {
|
||||
"@babel/runtime": ["@babel/runtime@7.3.4", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.4.0", "", {}, "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="],
|
||||
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
@@ -53,22 +49,32 @@
|
||||
|
||||
"asn1": ["asn1@0.2.3", "", {}, "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w=="],
|
||||
|
||||
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="],
|
||||
|
||||
"async-retry": ["async-retry@1.2.3", "", { "dependencies": { "retry": "0.12.0" } }, "sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="],
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="],
|
||||
|
||||
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
|
||||
|
||||
"babel-runtime": ["babel-runtime@6.26.0", "", { "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
||||
"bitsyntax": ["bitsyntax@0.0.4", "", { "dependencies": { "buffer-more-ints": "0.0.2" } }, "sha512-Pav3HSZXD2NLQOWfJldY3bpJLt8+HS2nUo5Z1bLLmHg2vCE/cM1qfEvNjlYo7GgYQPneNr715Bh42i01ZHZPvw=="],
|
||||
|
||||
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||
@@ -81,7 +87,7 @@
|
||||
|
||||
"buffer-more-ints": ["buffer-more-ints@0.0.2", "", {}, "sha512-PDgX2QJgUc5+Jb2xAoBFP5MxhtVUmZHR33ak+m/SDxRdCrbnX1BggRIaxiW7ImwfmO4iJeCQKN18ToSXWGjYkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||
|
||||
"bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
|
||||
@@ -91,16 +97,18 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
|
||||
|
||||
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
||||
|
||||
"chalk": ["chalk@2.4.1", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"commands-events": ["commands-events@1.0.4", "", { "dependencies": { "@babel/runtime": "7.2.0", "formats": "1.0.0", "uuidv4": "2.0.0" } }, "sha512-HdP/+1Anoc7z+6L2h7nd4Imz54+LW+BjMGt30riBZrZ3ZeP/8el93wD8Jj8ltAaqVslqNgjX6qlhSBJwuDSmpg=="],
|
||||
|
||||
"comparejs": ["comparejs@1.0.0", "", {}, "sha512-Ue/Zd9aOucHzHXwaCe4yeHR7jypp7TKrIBZ5yls35nPNiVXlW14npmNVKM1ZaLlQTKZ6/4ewA//gYKHHIwCpOw=="],
|
||||
@@ -125,7 +133,7 @@
|
||||
|
||||
"crypto2": ["crypto2@2.0.0", "", { "dependencies": { "babel-runtime": "6.26.0", "node-rsa": "0.4.2", "util.promisify": "1.0.0" } }, "sha512-jdXdAgdILldLOF53md25FiQ6ybj2kUFTiRjs7msKTUoZrzgT/M1FPX5dYGJjbbwFls+RJIiZxNTC02DE/8y0ZQ=="],
|
||||
|
||||
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
|
||||
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
@@ -135,33 +143,35 @@
|
||||
|
||||
"datasette": ["datasette@1.0.1", "", { "dependencies": { "comparejs": "1.0.0", "eventemitter2": "5.0.1", "lodash": "4.17.5" } }, "sha512-aJdlCBToEJUP4M57r67r4V6tltwGKa3qetnjpBtXYIlqbX9tM9jsoDMxb4xd9AGjpp3282oHRmqI5Z8TVAU0Mg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
"debug": ["debug@0.7.4", "", {}, "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"destroy": ["destroy@1.0.4", "", {}, "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg=="],
|
||||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
|
||||
"draht": ["draht@1.0.1", "", { "dependencies": { "eventemitter2": "5.0.1" } }, "sha512-yNNHL864dniNmIE9ZKD++mKypiAUAvVZtyV0QrbXH/ak3ebzFqo5xsmRBRqV8pZVhImOSBiyq500Wcmrf44zAg=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||
|
||||
"encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
||||
|
||||
"es-array-method-boxes-properly": ["es-array-method-boxes-properly@1.0.0", "", {}, "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="],
|
||||
|
||||
@@ -187,6 +197,10 @@
|
||||
|
||||
"express": ["express@4.16.4", "", { "dependencies": { "accepts": "~1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.3", "content-disposition": "0.5.2", "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.4", "qs": "6.5.2", "range-parser": "~1.2.0", "safe-buffer": "5.1.2", "send": "0.16.2", "serve-static": "1.13.2", "setprototypeof": "1.1.0", "statuses": "~1.4.0", "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
@@ -195,10 +209,16 @@
|
||||
|
||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||
|
||||
"first-chunk-stream": ["first-chunk-stream@0.1.0", "", {}, "sha512-o7kVqimu9cl+XNeEGqDPI8Ms4IViicBnjIDZ5uU+7aegfDhJJiU1Da9y52Qt0TfBO3rpKA5hW2cqwp4EkCfl9w=="],
|
||||
|
||||
"flaschenpost": ["flaschenpost@1.1.3", "", { "dependencies": { "@babel/runtime": "7.2.0", "app-root-path": "2.1.0", "babel-runtime": "6.26.0", "chalk": "2.4.1", "find-root": "1.1.0", "lodash": "4.17.11", "moment": "2.22.2", "processenv": "1.1.0", "split2": "3.0.0", "stack-trace": "0.0.10", "stringify-object": "3.3.0", "untildify": "3.0.3", "util.promisify": "1.0.0", "varname": "2.0.3" }, "bin": { "flaschenpost-uncork": "dist/bin/flaschenpost-uncork.js", "flaschenpost-normalize": "dist/bin/flaschenpost-normalize.js" } }, "sha512-1VAYPvDsVBGFJyUrOa/6clnJwZYC3qVq9nJLcypy6lvaaNbo1wOQiH8HQ+4Fw/k51pVG7JHzSf5epb8lmIW86g=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="],
|
||||
|
||||
"form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="],
|
||||
|
||||
"formats": ["formats@1.0.0", "", {}, "sha512-For0Y8egwEK96JgJo4NONErPhtl7H2QzeB2NYGmzeGeJ8a1JZqPgLYOtM3oJRCYhmgsdDFd6KGRYyfe37XY4Yg=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
@@ -223,10 +243,16 @@
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"har-schema": ["har-schema@2.0.0", "", {}, "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="],
|
||||
|
||||
"har-validator": ["har-validator@5.1.5", "", { "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
@@ -243,18 +269,18 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
"hono": ["hono@4.12.0", "", {}, "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA=="],
|
||||
|
||||
"http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="],
|
||||
|
||||
"http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.23", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"ioredis": ["ioredis@5.8.2", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
@@ -297,6 +323,10 @@
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
|
||||
|
||||
"is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
@@ -305,26 +335,32 @@
|
||||
|
||||
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||
|
||||
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
|
||||
|
||||
"jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
|
||||
|
||||
"json-lines": ["json-lines@1.0.0", "", { "dependencies": { "timer2": "1.0.0" } }, "sha512-ytuLZb4RBQb3bTRsG/QBenyIo5oHLpjeCVph3s2NnoAsZE9K6h+uR+OWpEOWV1UeHdX63tYctGppBpGAc+JNMA=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@8.5.0", "", { "dependencies": { "jws": "^3.2.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": "^5.6.0" } }, "sha512-IqEycp0znWHNA11TpYi77bVgyBO/pGESDh7Ajhas+u0ttkGkKYIIAjniL4Bw5+oVejVF+SYkaI7XKfwCCyeTuA=="],
|
||||
|
||||
"jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"jws": ["jws@3.2.3", "", { "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g=="],
|
||||
|
||||
"limes": ["limes@2.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "jsonwebtoken": "8.5.0" } }, "sha512-evWD0pnTgPX7QueaSoJl5JBUL30T1ZVzo34ke97tIKmeagqhBTYK/JkKL0vtG3MpNApw8ZY9TlbybfwEz9knBA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
"lodash": ["lodash@3.10.1", "", {}, "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
@@ -349,7 +385,7 @@
|
||||
|
||||
"mime": ["mime@1.4.1", "", { "bin": { "mime": "cli.js" } }, "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
@@ -357,7 +393,7 @@
|
||||
|
||||
"morgan": ["morgan@1.9.1", "", { "dependencies": { "basic-auth": "~2.0.0", "debug": "2.6.9", "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" } }, "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
@@ -371,6 +407,8 @@
|
||||
|
||||
"node-statsd": ["node-statsd@0.1.1", "", {}, "sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg=="],
|
||||
|
||||
"oauth-sign": ["oauth-sign@0.9.0", "", {}, "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
@@ -393,9 +431,9 @@
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.7", "", {}, "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
@@ -407,9 +445,11 @@
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
"qs": ["qs@6.5.5", "", {}, "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
@@ -417,16 +457,14 @@
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.12.1", "", {}, "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
@@ -439,6 +477,10 @@
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
|
||||
|
||||
"selectn": ["selectn@0.9.6", "", {}, "sha512-XOdvku6f41EAVbZGYTTyPp0CHWmUVYNuq81Hgy+4zEBZqFVk55rD1E3t1mqJZF0qZG40+PCJd+DQ8zUFy4v5Jg=="],
|
||||
|
||||
"semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"send": ["send@0.16.2", "", { "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", "destroy": "~1.0.4", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" } }, "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw=="],
|
||||
@@ -463,13 +505,15 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"soap": ["soap@0.12.0", "", { "dependencies": { "debug": "~0.7.4", "lodash": "3.x.x", "request": ">=2.9.0", "sax": ">=0.6", "selectn": "^0.9.6", "strip-bom": "~0.3.1" } }, "sha512-rAy5FjvlAB0lnup4+cuJP9dq/Av5jzV9fuJZUW4R5Z5POrzrL1pXTsWUGJV9ua15jum40iscM/rYHXhUCp/vWQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@3.0.0", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"statuses": ["statuses@1.4.0", "", {}, "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="],
|
||||
|
||||
@@ -487,16 +531,24 @@
|
||||
|
||||
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
|
||||
|
||||
"strip-bom": ["strip-bom@0.3.1", "", { "dependencies": { "first-chunk-stream": "^0.1.0", "is-utf8": "^0.2.0" }, "bin": { "strip-bom": "cli.js" } }, "sha512-8m24eJUyKXllSCydAwFVbr4QRZrRb82T2QfwtbO9gTLWhWIOxoDEZESzCGMgperFNyLhly6SDOs+LPH6/seBfw=="],
|
||||
|
||||
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"tailwind": ["tailwind@4.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "ajv": "6.10.0", "app-root-path": "2.1.0", "async-retry": "1.2.3", "body-parser": "1.18.3", "commands-events": "1.0.4", "compression": "1.7.3", "content-type": "1.0.4", "cors": "2.8.5", "crypto2": "2.0.0", "datasette": "1.0.1", "draht": "1.0.1", "express": "4.16.4 ", "flaschenpost": "1.1.3", "hase": "2.0.0", "json-lines": "1.0.0", "limes": "2.0.0", "lodash": "4.17.11", "lusca": "1.6.1", "morgan": "1.9.1", "nocache": "2.0.0", "partof": "1.0.0", "processenv": "1.1.0", "stethoskop": "1.0.0", "timer2": "1.0.0", "uuidv4": "3.0.1", "ws": "6.2.0" } }, "sha512-LlUNoD/5maFG1h5kQ6/hXfFPdcnYw+1Z7z+kUD/W/E71CUMwcnrskxiBM8c3G8wmPsD1VvCuqGYMHviI8+yrmg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
"tailwindcss": ["tailwindcss@4.2.0", "", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="],
|
||||
|
||||
"timer2": ["timer2@1.0.0", "", {}, "sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg=="],
|
||||
|
||||
"tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
|
||||
|
||||
"tsscmp": ["tsscmp@1.0.6", "", {}, "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
@@ -511,7 +563,7 @@
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
@@ -527,7 +579,7 @@
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
"uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
|
||||
|
||||
"uuidv4": ["uuidv4@3.0.1", "", { "dependencies": { "uuid": "3.3.2" } }, "sha512-PPzksdWRl2a5C9hrs3OOYrArTeyoR0ftJ3jtOy+BnVHkT2UlrrzPNt9nTdiGuxmQItHM/AcTXahwZZC57Njojg=="],
|
||||
|
||||
@@ -535,41 +587,55 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
|
||||
|
||||
"ws": ["ws@6.2.0", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w=="],
|
||||
|
||||
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||
|
||||
"babel-runtime/regenerator-runtime": ["regenerator-runtime@0.11.1", "", {}, "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="],
|
||||
|
||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"body-parser/qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
|
||||
"commands-events/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"commands-events/uuidv4": ["uuidv4@2.0.0", "", { "dependencies": { "sha-1": "0.1.1", "uuid": "3.3.2" } }, "sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q=="],
|
||||
|
||||
"compressible/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"datasette/lodash": ["lodash@4.17.5", "", {}, "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="],
|
||||
|
||||
"ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"express/qs": ["qs@6.5.2", "", {}, "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"flaschenpost/@babel/runtime": ["@babel/runtime@7.2.0", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg=="],
|
||||
|
||||
"flaschenpost/lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"har-validator/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"hase/@babel/runtime": ["@babel/runtime@7.1.2", "", { "dependencies": { "regenerator-runtime": "^0.12.0" } }, "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg=="],
|
||||
|
||||
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
"jsonwebtoken/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
@@ -579,22 +645,20 @@
|
||||
|
||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"send/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"tailwind/lodash": ["lodash@4.17.11", "", {}, "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="],
|
||||
|
||||
"uuidv4/uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
|
||||
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||
|
||||
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
"commands-events/uuidv4/uuid": ["uuid@3.3.2", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
"har-validator/ajv/fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mode3 - QuickBooks Integration</title>
|
||||
<link rel="stylesheet" href="output.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.status-label {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.status-ok {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
button:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
button.danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
button.danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
.form-group {
|
||||
margin: 15px 0;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.message {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-top: 15px;
|
||||
display: none;
|
||||
}
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
display: block;
|
||||
}
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔌 Mode3 - QuickBooks Integration</h1>
|
||||
<p class="subtitle">SOAP Server for QuickBooks Web Connector</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Server Status</h2>
|
||||
<p>Status: <span id="serverStatus" class="status-label status-ok">✓ Running</span></p>
|
||||
<p>Mode: <strong>Mode3</strong></p>
|
||||
<p>Endpoint: <strong>http://localhost:3005/ws</strong></p>
|
||||
<button onclick="checkStatus()">Refresh Status</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>QuickBooks Authentication</h2>
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" placeholder="Enter username" value="admin">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" placeholder="Enter password" value="password">
|
||||
</div>
|
||||
<button onclick="authenticate()">Authenticate with QB</button>
|
||||
<div id="authMessage" class="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Session Info</h2>
|
||||
<p>Current Session: <span id="sessionStatus" class="status-label status-error">Not authenticated</span></p>
|
||||
<div id="sessionDetails" style="display: none;">
|
||||
<p><strong>Company File:</strong> <span id="companyFile"></span></p>
|
||||
<p><strong>User:</strong> <span id="sessionUser"></span></p>
|
||||
<p><strong>Expires:</strong> <span id="sessionExpires"></span></p>
|
||||
<button class="danger" onclick="logout()">🔌 Close QB Connection / Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Enter Credit Card Charge</h2>
|
||||
<div class="form-group">
|
||||
<label for="ccAccount">Credit Card Account:</label>
|
||||
<input type="text" id="ccAccount" placeholder="e.g., 21010 - Kum&Go Fleet Card" value="21010 - Kum&Go Fleet Card">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vendor">Purchased From (Vendor):</label>
|
||||
<input type="text" id="vendor" placeholder="e.g., Gas Station">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="chargeDate">Date:</label>
|
||||
<input type="text" id="chargeDate" placeholder="MM/DD/YYYY">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="refNum">Ref No:</label>
|
||||
<input type="text" id="refNum" placeholder="e.g., 123456789">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount ($):</label>
|
||||
<input type="text" id="amount" placeholder="e.g., 12.00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="expenseAccount">Expense Account:</label>
|
||||
<input type="text" id="expenseAccount" placeholder="e.g., 60000 - Semi-Variable Expenses:50160 - Gas/Fuel">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="memo">Memo:</label>
|
||||
<input type="text" id="memo" placeholder="Optional memo">
|
||||
</div>
|
||||
<button onclick="addCharge()">Add Charge</button>
|
||||
<div id="chargeMessage" class="message"></div>
|
||||
<button onclick="viewCharges()" style="background: #764ba2;">View Queued Charges</button>
|
||||
<div id="chargesList" style="display: none; margin-top: 20px;">
|
||||
<h3>Pending Charges</h3>
|
||||
<div id="chargesContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Documentation</h2>
|
||||
<ul>
|
||||
<li><strong>SOAP Endpoint:</strong> <code>POST /ws</code> - QuickBooks Web Connector SOAP interface</li>
|
||||
<li><strong>WSDL:</strong> <code>GET /ws</code> - WSDL definition for QB Web Connector</li>
|
||||
<li><strong>Status:</strong> <code>GET /quickbooks/status</code> - Check authentication status</li>
|
||||
<li><strong>Auth:</strong> <code>POST /quickbooks/auth</code> - REST authentication endpoint</li>
|
||||
<li><strong>Health:</strong> <code>GET /health</code> - Server health check</li>
|
||||
<li><strong>Charges:</strong> <code>POST /api/charges</code> - Submit credit card charge</li>
|
||||
<li><strong>Charges List:</strong> <code>GET /api/charges</code> - List all charges</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set today's date as default
|
||||
function setDefaultDate() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('chargeDate').value = today;
|
||||
}
|
||||
|
||||
async function addCharge() {
|
||||
const msgDiv = document.getElementById('chargeMessage');
|
||||
try {
|
||||
const charge = {
|
||||
creditCardAccount: document.getElementById('ccAccount').value || '21010 - Kum&Go Fleet Card',
|
||||
vendor: document.getElementById('vendor').value || 'Unknown Vendor',
|
||||
amount: document.getElementById('amount').value,
|
||||
expenseAccount: document.getElementById('expenseAccount').value || '60000 - Semi-Variable Expenses',
|
||||
date: document.getElementById('chargeDate').value,
|
||||
refNum: document.getElementById('refNum').value || undefined,
|
||||
memo: document.getElementById('memo').value || undefined
|
||||
};
|
||||
|
||||
if (!charge.vendor || !charge.amount) {
|
||||
msgDiv.className = 'message error';
|
||||
msgDiv.textContent = '✗ Vendor and Amount are required';
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch('/api/charges', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(charge)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
msgDiv.className = 'message success';
|
||||
msgDiv.textContent = '✓ ' + data.message + ' (ID: ' + data.charge.id + ')';
|
||||
// Clear form
|
||||
document.getElementById('vendor').value = '';
|
||||
document.getElementById('amount').value = '';
|
||||
document.getElementById('refNum').value = '';
|
||||
document.getElementById('memo').value = '';
|
||||
setDefaultDate();
|
||||
viewCharges();
|
||||
} else {
|
||||
msgDiv.className = 'message error';
|
||||
msgDiv.textContent = '✗ ' + data.error;
|
||||
}
|
||||
} catch (error) {
|
||||
msgDiv.className = 'message error';
|
||||
msgDiv.textContent = '✗ Failed to add charge: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function viewCharges() {
|
||||
const container = document.getElementById('chargesContainer');
|
||||
try {
|
||||
const res = await fetch('/api/charges');
|
||||
const data = await res.json();
|
||||
|
||||
let html = `<p><strong>Total Charges: ${data.total}</strong></p>`;
|
||||
|
||||
if (data.pending.length > 0) {
|
||||
html += '<h4>Pending (Not Yet Sent)</h4><ul>';
|
||||
data.pending.forEach(ch => {
|
||||
html += `<li>${ch.vendor} - $${ch.amount} on ${ch.date} (${ch.id})</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
if (data.sent.length > 0) {
|
||||
html += '<h4>Sent to QB (Awaiting Processing)</h4><ul>';
|
||||
data.sent.forEach(ch => {
|
||||
html += `<li>${ch.vendor} - $${ch.amount} on ${ch.date} (${ch.id})</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
if (data.total === 0) {
|
||||
html += '<p><em>No charges queued</em></p>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
document.getElementById('chargesList').style.display = 'block';
|
||||
} catch (error) {
|
||||
container.innerHTML = '<p class="error">Failed to load charges</p>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function checkStatus() {
|
||||
try {
|
||||
const res = await fetch('/health');
|
||||
const data = await res.json();
|
||||
document.getElementById('serverStatus').className = 'status-label status-ok';
|
||||
document.getElementById('serverStatus').textContent = '✓ Running';
|
||||
updateSessionInfo();
|
||||
} catch (error) {
|
||||
document.getElementById('serverStatus').className = 'status-label status-error';
|
||||
document.getElementById('serverStatus').textContent = '✗ Offline';
|
||||
}
|
||||
}
|
||||
|
||||
// ... rest of existing code remains the same ...
|
||||
|
||||
// Check status on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setDefaultDate();
|
||||
checkStatus();
|
||||
updateSessionInfo();
|
||||
});
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const messageDiv = document.getElementById('authMessage');
|
||||
|
||||
try {
|
||||
const res = await fetch('/quickbooks/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
messageDiv.className = 'message success';
|
||||
messageDiv.textContent = '✓ ' + data.message;
|
||||
setTimeout(() => updateSessionInfo(), 500);
|
||||
} else {
|
||||
messageDiv.className = 'message error';
|
||||
messageDiv.textContent = '✗ ' + data.error;
|
||||
}
|
||||
} catch (error) {
|
||||
messageDiv.className = 'message error';
|
||||
messageDiv.textContent = '✗ Connection failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSessionInfo() {
|
||||
try {
|
||||
const res = await fetch('/quickbooks/status');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
document.getElementById('sessionStatus').className = 'status-label status-ok';
|
||||
document.getElementById('sessionStatus').textContent = '✓ Authenticated';
|
||||
document.getElementById('companyFile').textContent = data.companyFile;
|
||||
document.getElementById('sessionUser').textContent = data.userName;
|
||||
document.getElementById('sessionExpires').textContent = data.sessionExpires;
|
||||
document.getElementById('sessionDetails').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('sessionStatus').className = 'status-label status-error';
|
||||
document.getElementById('sessionStatus').textContent = '✗ Not authenticated';
|
||||
document.getElementById('sessionDetails').style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update session info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const res = await fetch('/quickbooks/logout', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
updateSessionInfo();
|
||||
document.getElementById('authMessage').className = 'message success';
|
||||
const timestamp = data.closedAt ? ` (${new Date(data.closedAt).toLocaleTimeString()})` : '';
|
||||
document.getElementById('authMessage').textContent = '✓ ' + data.message + timestamp;
|
||||
} else {
|
||||
document.getElementById('authMessage').className = 'message error';
|
||||
document.getElementById('authMessage').textContent = '✗ ' + (data.error || 'Logout failed');
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('authMessage').className = 'message error';
|
||||
document.getElementById('authMessage').textContent = '✗ Logout failed: ' + error.message;
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check status on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkStatus();
|
||||
updateSessionInfo();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user