Fix PocketBase auth and note submission

- Replace popup OAuth with SDK authWithOAuth2 realtime channel (urlCallback)
  so login works in Edge app mode without navigating the main window
- Remove manual redirect/sessionStorage OAuth flow that broke in Edge app mode
- Serve PocketBase config (URL, collection, provider) from server endpoint
  backed by env vars instead of hardcoded localhost
- Fix /api/submit: target Notes collection with correct field mapping
  (body_plain, body_html, title, type, email, Username, userId, job_note,
  Job_Number) instead of Job_Info_Prod with wrong field names
- Remove localhost fallbacks from auth-module backend and frontend
This commit is contained in:
2026-03-24 18:29:51 +00:00
parent c76739d0d7
commit 513e975530
4 changed files with 182 additions and 40 deletions
+127 -26
View File
@@ -19,14 +19,45 @@
border-radius: 0.75rem;
padding: 0.55rem 1rem;
font-weight: 600;
transition: all 120ms ease;
transition: all 160ms ease;
position: relative;
overflow: hidden;
}
.btn:focus-visible,
.field:focus-visible {
outline: 2px solid rgb(34 211 238 / 0.8);
outline-offset: 1px;
}
.btn:hover { transform: translateY(-1px); }
.btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(120deg, transparent 0%, rgb(255 255 255 / 0.2) 48%, transparent 100%);
transform: translateX(-130%);
transition: transform 260ms ease;
pointer-events: none;
}
.btn:hover {
transform: translateY(-1px) scale(1.01);
box-shadow: 0 10px 22px rgb(15 23 42 / 0.28);
}
.btn:hover::before { transform: translateX(130%); }
.btn-accent-cyan {
color: rgb(8 47 73 / 1);
background: rgb(6 182 212 / 1);
}
.btn-accent-emerald {
color: rgb(6 44 32 / 1);
background: rgb(34 197 94 / 1);
}
.btn-accent-indigo {
color: rgb(255 255 255 / 0.98);
background: rgb(99 102 241 / 1);
}
.btn-accent-violet {
color: rgb(255 255 255 / 0.98);
background: rgb(139 92 246 / 1);
}
.btn-secondary {
border: 1px solid rgb(51 65 85 / 1);
background: rgb(15 23 42 / 1);
@@ -42,7 +73,21 @@
font-size: 0.875rem;
color: rgb(248 250 252 / 1);
}
.field option {
background: rgb(15 23 42 / 0.96);
color: rgb(248 250 252 / 1);
}
.field::placeholder { color: rgb(100 116 139 / 1); }
#output {
border: 1px solid rgb(30 41 59 / 1);
background: rgb(2 6 23 / 1);
}
code {
border: 1px solid rgb(51 65 85 / 1);
background: rgb(15 23 42 / 1);
padding: 0.12rem 0.35rem;
border-radius: 0.45rem;
}
</style>
</head>
<body class="min-h-screen bg-gradient-to-b from-slate-950 via-slate-950 to-slate-900 text-slate-100">
@@ -78,7 +123,7 @@
<h2 class="text-xl font-medium">1) Prism Notes Login</h2>
<p class="mt-2 text-sm text-slate-300">Uses PocketBase OAuth with Microsoft to validate the Prism Notes user session.</p>
<div class="mt-4 flex flex-wrap gap-3">
<button id="loginBtn" class="btn bg-cyan-500 text-slate-950 hover:bg-cyan-400">Login with Microsoft</button>
<button id="loginBtn" class="btn btn-accent-cyan">Login with Microsoft</button>
<button id="logoutBtn" class="btn btn-secondary">Logout</button>
</div>
</article>
@@ -88,7 +133,7 @@
<p class="mt-2 text-sm text-slate-300">Gets delegated Graph token used for OneNote APIs.</p>
<p id="oneNoteAuthStatus" class="mt-3 text-sm font-medium">Disconnected</p>
<div class="mt-4 flex flex-wrap gap-3">
<button id="connectOneNoteBtn" class="btn bg-emerald-500 text-slate-950 hover:bg-emerald-400">Connect OneNote</button>
<button id="connectOneNoteBtn" class="btn btn-accent-emerald">Connect OneNote</button>
<button id="refreshOneNoteTokenBtn" class="btn btn-secondary">Refresh token</button>
</div>
</article>
@@ -120,8 +165,8 @@
</div>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<button id="submitNoteBtn" class="btn bg-indigo-500 text-white hover:bg-indigo-400">Submit to Prism Notes</button>
<button id="syncOneNoteBtn" class="btn bg-violet-500 text-white hover:bg-violet-400">Sync to OneNote</button>
<button id="submitNoteBtn" class="btn btn-accent-indigo">Submit to Prism Notes</button>
<button id="syncOneNoteBtn" class="btn btn-accent-violet">Sync to OneNote</button>
</div>
</article>
@@ -146,7 +191,7 @@
<label class="text-sm font-medium text-slate-300">Proof Message</label>
<textarea id="proofBody" rows="6" class="field mt-1"></textarea>
</div>
<button id="createProofPageBtn" class="btn bg-emerald-500 text-slate-950 hover:bg-emerald-400">Create OneNote Proof Page</button>
<button id="createProofPageBtn" class="btn btn-accent-emerald">Create OneNote Proof Page</button>
</div>
</article>
</section>
@@ -160,11 +205,12 @@
<script type="module">
import PocketBase from 'https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/+esm';
const PB_URL = 'http://127.0.0.1:8090';
const PB_AUTH_COLLECTION = 'Users';
const PB_DEFAULT_AUTH_COLLECTION = 'Users';
const PB_DEFAULT_OAUTH_PROVIDER = 'microsoft';
const SCOPES = ['User.Read', 'Notes.ReadWrite.All', 'Sites.Read.All', 'offline_access'];
const pb = new PocketBase(PB_URL);
let pb = null;
let pbConfigPromise = null;
const output = document.getElementById('output');
const healthStatus = document.getElementById('healthStatus');
@@ -194,6 +240,35 @@
let delegatedAccessToken = '';
let delegatedExpiresAt = 0;
async function getPocketBaseConfig() {
if (!pbConfigPromise) {
pbConfigPromise = fetch('/api/auth/pocketbase-config')
.then(async (resp) => {
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success || !data?.pbUrl) {
throw new Error(data?.message || 'PocketBase config unavailable');
}
return {
pbUrl: data.pbUrl,
collection: data.collection || PB_DEFAULT_AUTH_COLLECTION,
provider: data.provider || PB_DEFAULT_OAUTH_PROVIDER,
};
})
.catch((error) => {
pbConfigPromise = null;
throw error;
});
}
return pbConfigPromise;
}
async function ensurePocketBaseClient() {
if (pb) return pb;
const config = await getPocketBaseConfig();
pb = new PocketBase(config.pbUrl);
return pb;
}
function writeOutput(value) {
output.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
}
@@ -207,9 +282,10 @@
.replace(/'/g, '&#39;');
}
function updatePbStatus() {
if (pb.authStore?.isValid) {
const email = pb.authStore.model?.email || '(unknown user)';
async function updatePbStatus() {
const client = await ensurePocketBaseClient();
if (client.authStore?.isValid) {
const email = client.authStore.model?.email || '(unknown user)';
pbStatus.textContent = `Logged in: ${email}`;
} else {
pbStatus.textContent = 'Not logged in';
@@ -352,15 +428,36 @@
}
async function loginPocketBase() {
writeOutput('Opening PocketBase Microsoft login...');
await pb.collection(PB_AUTH_COLLECTION).authWithOAuth2({ provider: 'microsoft' });
updatePbStatus();
writeOutput({ message: 'PocketBase login completed.', user: pb.authStore.model?.email || null });
writeOutput('Opening Microsoft login...');
const client = await ensurePocketBaseClient();
const config = await getPocketBaseConfig();
// Use the SDK's authWithOAuth2 realtime channel.
// Auth result is delivered back to this window via SSE from PocketBase,
// so it works regardless of how/where the login popup opens (including
// Edge app mode where external URLs open outside the app window).
const authData = await client.collection(config.collection).authWithOAuth2({
provider: config.provider,
urlCallback(url) {
const w = Math.min(900, window.screen.availWidth || 900);
const h = Math.min(680, window.screen.availHeight || 680);
const left = Math.floor(((window.screen.availWidth || 1280) - w) / 2);
const top = Math.floor(((window.screen.availHeight || 800) - h) / 2);
window.open(url, 'pb_oauth', `width=${w},height=${h},top=${top},left=${left},resizable=yes,menubar=no`);
},
});
await updatePbStatus();
writeOutput({
message: 'PocketBase login completed.',
user: authData?.record?.email || client.authStore.model?.email || null,
});
}
function logoutPocketBase() {
pb.authStore.clear();
updatePbStatus();
async function logoutPocketBase() {
const client = await ensurePocketBaseClient();
client.authStore.clear();
await updatePbStatus();
writeOutput({ message: 'PocketBase session cleared.' });
}
@@ -375,13 +472,14 @@
title,
bodyPlain,
noteType: noteType.value || 'personal',
userEmail: pb.authStore.model?.email || '',
userEmail: pb?.authStore.model?.email || '',
jobNumber: noteJobNumber.value.trim(),
};
}
async function submitNoteToPrismNotes() {
if (!pb.authStore.isValid || !pb.authStore.token) {
const client = await ensurePocketBaseClient();
if (!client.authStore.isValid || !client.authStore.token) {
throw new Error('PocketBase login required');
}
const payload = buildNotePayload();
@@ -390,7 +488,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...payload,
pbToken: pb.authStore.token,
pbToken: client.authStore.token,
}),
});
const data = await resp.json().catch(() => ({}));
@@ -401,7 +499,8 @@
}
async function syncCurrentNoteToOneNote() {
if (!pb.authStore.isValid || !pb.authStore.token) {
const client = await ensurePocketBaseClient();
if (!client.authStore.isValid || !client.authStore.token) {
throw new Error('PocketBase login required');
}
if (!hasUsableToken()) {
@@ -414,7 +513,7 @@
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${delegatedAccessToken}`,
'x-pb-token': pb.authStore.token,
'x-pb-token': client.authStore.token,
},
body: JSON.stringify(payload),
});
@@ -505,7 +604,9 @@
bind('resolveSectionsBtn', resolveSiteSections);
bind('createProofPageBtn', createProofPage);
updatePbStatus();
ensurePocketBaseClient()
.then(() => updatePbStatus())
.catch((error) => writeOutput({ error: error?.message || String(error) }));
updateOneNoteStatus();
loadHealth();
loadGraphStatus();