diff --git a/.env b/.env new file mode 100644 index 0000000..8d236b1 --- /dev/null +++ b/.env @@ -0,0 +1,16 @@ +# Microsoft Azure AD Configuration +CLIENT_SECRET=7aD8Q~d5K~_PzQv6KqDdrEnmyXHE60eVDpbcnaK_ +TENANT_ID=3fd97ea7-b124-41f1-855f-52d8ac3b16c7 +CLIENT_ID=3c846e71-9609-40e1-b458-0eb805e21b9f +REDIRECT_URI=https://pocketbase.ccllc.pro/api/oauth2-redirect + +# PocketBase Configuration +PB_DB=https://pocketbase.ccllc.pro +PB_AUTH_COLLECTION=Users + +#PocketBase Tables +PB_Job_Collection=Job_Info_TestEnv +PB_Notes_Collection=Notes +PB_App_Preferences_Collection=app_preferences_settings +PB_User_Preferences_Collection=user_preferences_settings +PB_Attachments_Collection=Attachments diff --git a/README.md b/README.md index 59fda6f..183d0e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ -# Prism-Notes +# Prism Notes +PocketBase + Microsoft Graph OAuth capture app. Frontend is a Tailwind-powered note and audio capture page; backend is a Bun/Hono server that validates PocketBase tokens and can mint Graph tokens (client credentials flow). + +## What you get +- `server.ts` — Bun + Hono server with `/health`, `/api/graph/status`, `/api/submit`, and static `index.html` serving. +- `index.html` — Microsoft OAuth via PocketBase plus note/audio capture UI. +- `tsconfig.json`, `package.json`, `bun.lock` — project configuration. + +## Setup +1) Install Bun (https://bun.sh). +2) Add a `.env` file at the project root with the variables below. +3) Install deps: `bun install` +4) Run dev server: `bun run --watch server.ts` (default port 5500). + +Open http://localhost:5500 and sign in with Microsoft. + +## Environment variables +| Name | Description | +| --- | --- | +| `CLIENT_ID` | Azure AD app (client) ID | +| `CLIENT_SECRET` | Azure AD client secret | +| `TENANT_ID` | Azure AD tenant ID | +| `REDIRECT_URI` | Redirect URI configured in the Azure app | +| `PB_DB` | PocketBase URL (e.g., http://127.0.0.1:8090) | +| `PB_COLLECTION` | Collection to store submissions (default `Job_Info_TestEnv`) | +| `PB_AUTH_COLLECTION` | Auth collection for token refresh (default `Users`) | +| `PORT` | Server port (default 5500) | + +## Notes +- `getGraphToken()` in `server.ts` is ready for Graph API calls; extend with additional routes as needed. +- `/api/submit` validates the provided `pbToken` then writes the payload to `PB_COLLECTION`; adjust fields to match your schema. +- Static file serving uses `index.html` from the repo root. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..b0f2ea1 --- /dev/null +++ b/bun.lock @@ -0,0 +1,77 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "pbandgraph-scaffold", + "dependencies": { + "@azure/msal-node": "^2.6.7", + "@microsoft/microsoft-graph-client": "^3.0.7", + "dotenv": "^17.2.3", + "hono": "^4.10.8", + "pocketbase": "^0.26.5", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "latest", + }, + }, + }, + "packages": { + "@azure/msal-common": ["@azure/msal-common@14.16.1", "", {}, "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w=="], + + "@azure/msal-node": ["@azure/msal-node@2.16.3", "", { "dependencies": { "@azure/msal-common": "14.16.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@microsoft/microsoft-graph-client": ["@microsoft/microsoft-graph-client@3.0.7", "", { "dependencies": { "@babel/runtime": "^7.12.5", "tslib": "^2.2.0" } }, "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw=="], + + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + + "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=="], + + "hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="], + + "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=="], + + "pocketbase": ["pocketbase@0.26.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="], + + "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=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "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=="], + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..fbb38d9 --- /dev/null +++ b/index.html @@ -0,0 +1,696 @@ + + + + + + PB + Microsoft Graph + + + + + + + + + + + +
+ v1.0.0-alpha3 +
+ + +
+
+

PB + Microsoft Graph

+

Sign in with Microsoft

+
+ +
+

Sign in with your Microsoft account

+ +
+ + +
+ + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..ffa819d --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "prism-notes", + "version": "1.0.0-alpha3", + "description": "Prism Notes: PocketBase + Microsoft Graph OAuth capture app", + "type": "module", + "scripts": { + "dev": "bun run --watch server.ts", + "start": "bun run server.ts" + }, + "dependencies": { + "@azure/msal-node": "^2.6.7", + "@microsoft/microsoft-graph-client": "^3.0.7", + "dotenv": "^16.4.5", + "hono": "^4.10.8", + "pocketbase": "^0.26.5" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "latest" + } +} diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..33e9f7d --- /dev/null +++ b/server.ts @@ -0,0 +1,135 @@ +import { Hono } from 'hono'; +import { serveStatic } from 'hono/bun'; +import { cors } from 'hono/cors'; +import PocketBase from 'pocketbase'; +import { ConfidentialClientApplication } from '@azure/msal-node'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +// Load env from local .env +const envPath = join(import.meta.dir, '.env'); +try { + const envContent = readFileSync(envPath, 'utf-8'); + envContent.split('\n').forEach(line => { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const [key, ...valueParts] = trimmed.split('='); + if (key && valueParts.length > 0) { + process.env[key.trim()] = valueParts.join('=').trim(); + } + } + }); + console.log('✓ Loaded env from .env'); +} catch (e) { + console.warn('⚠ Could not load .env:', (e as Error).message); +} + +const app = new Hono(); + +// Enable CORS +app.use('/*', cors({ + origin: '*', + credentials: true, +})); + +// PocketBase client +const pb = new PocketBase(process.env.PB_DB || 'http://127.0.0.1:8090'); + +// Use user's PocketBase token +function setUserPocketBaseAuth(token: string) { + pb.authStore.save(token, null); +} + +// MSAL config for Graph token (client credentials) +const msalConfig = { + auth: { + clientId: process.env.CLIENT_ID || '', + authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`, + clientSecret: process.env.CLIENT_SECRET || '', + }, +}; +const cca = new ConfidentialClientApplication(msalConfig); + +// Simple in-memory cache for Graph token +let graphTokenCache: { token: string; expiresOn: number } | null = null; + +async function getGraphToken(): Promise<{ token: string; expiresOn: number }> { + const now = Date.now(); + if (graphTokenCache && graphTokenCache.expiresOn - 60000 > now) { + return graphTokenCache; + } + const result = await cca.acquireTokenByClientCredential({ + scopes: ['https://graph.microsoft.com/.default'], + }); + if (!result || !result.accessToken) { + throw new Error('Failed to acquire Graph token'); + } + const expiresOn = result.expiresOn ? new Date(result.expiresOn).getTime() : now + 55 * 60 * 1000; + graphTokenCache = { token: result.accessToken, expiresOn }; + return graphTokenCache; +} + +// Health check +app.get('/health', (c) => { + return c.json({ ok: true, pbDB: process.env.PB_DB }); +}); + +// Graph token status (acquires if missing/expired) +app.get('/api/graph/status', async (c) => { + try { + const tokenInfo = await getGraphToken(); + return c.json({ active: true, expiresOnISO: new Date(tokenInfo.expiresOn).toISOString() }); + } catch (e) { + return c.json({ active: false, error: (e as Error)?.message || 'Failed to acquire token' }, 500); + } +}); + +// Submit endpoint (example) +app.post('/api/submit', async (c) => { + try { + const body = await c.req.json(); + const pbToken = body.pbToken; + + if (!pbToken) { + return c.json({ success: false, message: 'Missing pbToken' }, 401); + } + + // Validate user token + setUserPocketBaseAuth(pbToken); + try { + await pb.collection(process.env.PB_AUTH_COLLECTION || 'Users').authRefresh(); + } catch (e) { + console.error('auth_validation_failed', { message: (e as Error)?.message }); + return c.json({ success: false, message: 'Invalid token' }, 401); + } + + // Store in PocketBase collection (adjust fields as needed) + const record = await pb + .collection(process.env.PB_COLLECTION || 'Job_Info_TestEnv') + .create({ + ...body, + submittedBy: pb.authStore.model?.email || 'unknown', + submittedAt: new Date().toISOString(), + }); + + console.log('submission_success', { recordId: record.id }); + return c.json({ success: true, message: 'Feedback submitted successfully', recordId: record.id }); + } catch (err) { + console.error('submit_error', { message: (err as Error)?.message }); + return c.json({ success: false, message: 'Internal error' }, 500); + } +}); + +// Serve static index.html +app.get('/', async (c) => { + const html = await Bun.file(new URL('./index.html', import.meta.url)).text(); + return c.html(html); +}); + +const PORT = Number(process.env.PORT || 5500); +console.log(`PBandGraph server running at http://localhost:${PORT}`); + +export default { + port: PORT, + fetch: app.fetch, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6f0efe0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["bun-types", "node", "bun"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "moduleDetection": "force", + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "esModuleInterop": true, + "resolveJsonModule": true + } +}