feat: improve OIDC login flow and stores handling
Some checks failed
TrafficCue CI / check (push) Failing after 1m58s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has started running

This commit is contained in:
2025-09-29 18:54:12 +02:00
parent 004ba9047f
commit f5e1e23cdd
6 changed files with 89 additions and 39 deletions

35
src/OIDCCallback.svelte Normal file
View File

@ -0,0 +1,35 @@
<script>
import { uploadID } from "$lib/services/lnv";
import { getOIDCUser } from "$lib/services/oidc";
import { onMount } from "svelte";
const url = new URL(location.href);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const storedState = localStorage.getItem("lnv-oidcstate");
const codeVerifier = localStorage.getItem("lnv-codeVerifier");
localStorage.removeItem("lnv-oidcstate");
localStorage.removeItem("lnv-codeVerifier");
async function login() {
if (!code || !state || !codeVerifier || !storedState) {
alert("Missing code, state, codeVerifier or storedState.");
return;
}
if (state !== storedState) {
alert("State mismatch. Please try again.");
return;
}
const token = await getOIDCUser(code, codeVerifier);
localStorage.setItem("lnv-id", token.id_token);
localStorage.setItem("lnv-token", token.access_token);
localStorage.setItem("lnv-refresh", token.refresh_token);
await uploadID();
}
onMount(async () => {
await login();
location.href = "/";
})
</script>

View File

@ -33,9 +33,9 @@
<Button <Button
onclick={async () => { onclick={async () => {
const auth = await getAuthURL(); const auth = await getAuthURL();
// localStorage.setItem("lnv-codeVerifier", auth.codeVerifier); localStorage.setItem("lnv-codeVerifier", auth.codeVerifier);
// localStorage.setItem("lnv-oidcstate", auth.state); localStorage.setItem("lnv-oidcstate", auth.state);
const popup = window.open(auth.url, "Login", "width=500,height=600"); location.href = auth.url;
window.addEventListener("message", async (e) => { window.addEventListener("message", async (e) => {
if (e.origin !== window.location.origin) return; if (e.origin !== window.location.origin) return;
@ -45,7 +45,6 @@
console.error("Invalid response from popup"); console.error("Invalid response from popup");
return; return;
} }
popup?.close();
if (state !== auth.state) { if (state !== auth.state) {
alert("State mismatch. Please try again."); alert("State mismatch. Please try again.");
return; return;

View File

@ -5,6 +5,7 @@
MapIcon, MapIcon,
PackageMinusIcon, PackageMinusIcon,
PackagePlusIcon, PackagePlusIcon,
PackageXIcon,
RefreshCcwIcon, RefreshCcwIcon,
SpeechIcon, SpeechIcon,
ToggleLeftIcon, ToggleLeftIcon,
@ -17,7 +18,7 @@
import { view } from "../../view.svelte"; import { view } from "../../view.svelte";
import { m } from "$lang/messages"; import { m } from "$lang/messages";
import { setOnboardingState } from "$lib/onboarding.svelte"; import { setOnboardingState } from "$lib/onboarding.svelte";
import { stores, syncStores, updateStore } from "$lib/services/stores.svelte"; import { getDB, stores, syncStores, updateStore } from "$lib/services/stores.svelte";
const dev = getDeveloperToggle(); const dev = getDeveloperToggle();
@ -108,6 +109,19 @@
await updateStore({ name, type }, null); await updateStore({ name, type }, null);
}} }}
/> />
<SettingsButton
icon={PackageXIcon}
text="Nuke all Stores"
onclick={async () => {
if (!confirm("Are you sure?")) return;
const db = await getDB();
const tx = db.transaction(["stores", "changes"], "readwrite");
await tx.objectStore("stores").clear();
await tx.objectStore("changes").clear();
await tx.done;
alert("Nuked all stores");
}}
/>
<span>LOCATION STORES: {JSON.stringify(locationStores.current)}</span> <span>LOCATION STORES: {JSON.stringify(locationStores.current)}</span>
</section> </section>

View File

@ -9,4 +9,4 @@ export const LNV_SERVER =
? "http://localhost:3000/api" ? "http://localhost:3000/api"
: location.hostname.includes("staging") : location.hostname.includes("staging")
? "https://staging-trafficcue-api.picoscratch.de/api" ? "https://staging-trafficcue-api.picoscratch.de/api"
: "https://trafficcue-api.picoscratch.de/api"; : "https://staging-trafficcue-api.picoscratch.de/api";

View File

@ -1,6 +1,6 @@
// import { getStores, putStore } from "./lnv"; // import { getStores, putStore } from "./lnv";
import { openDB, type DBSchema } from "idb"; import { openDB, type DBSchema, type IDBPDatabase } from "idb";
import { authFetch, hasCapability, ping } from "./lnv"; import { authFetch, hasCapability, ping } from "./lnv";
import { LNV_SERVER } from "./hosts"; import { LNV_SERVER } from "./hosts";
@ -44,7 +44,9 @@ interface TCDB extends DBSchema {
}; };
} }
export const db = await openDB<TCDB>("tc", 1, { export async function getDB(): Promise<IDBPDatabase<TCDB>> {
if (_db) return _db;
_db = await openDB<TCDB>("tc", 1, {
upgrade(db, _oldVersion, _newVersion, _transaction, _event) { upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
if (!db.objectStoreNames.contains("stores")) { if (!db.objectStoreNames.contains("stores")) {
const store = db.createObjectStore("stores", { keyPath: "id" }); const store = db.createObjectStore("stores", { keyPath: "id" });
@ -57,7 +59,11 @@ export const db = await openDB<TCDB>("tc", 1, {
db.createObjectStore("changes", { keyPath: "id" }); db.createObjectStore("changes", { keyPath: "id" });
} }
}, },
}); });
return _db;
}
let _db: IDBPDatabase<TCDB>;
const eventTarget = new EventTarget(); const eventTarget = new EventTarget();
@ -77,6 +83,10 @@ export async function syncStores() {
if (!(await hasCapability("stores"))) { if (!(await hasCapability("stores"))) {
return; return;
} }
if (!(localStorage.getItem("lnv-token"))) {
return;
}
const db = await getDB();
const changes = await Promise.all( const changes = await Promise.all(
await db.getAll("changes").then((changes) => await db.getAll("changes").then((changes) =>
changes.map(async (change) => { changes.map(async (change) => {
@ -133,6 +143,7 @@ export async function syncStores() {
} }
async function createStore(info: StoreInfo, data: object) { async function createStore(info: StoreInfo, data: object) {
const db = await getDB();
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const store: Store = { const store: Store = {
id, id,
@ -154,6 +165,7 @@ async function createStore(info: StoreInfo, data: object) {
} }
export async function updateStore(info: StoreInfo, data: object | null) { export async function updateStore(info: StoreInfo, data: object | null) {
const db = await getDB();
const store = await db.getFromIndex("stores", "by-name-and-type", [ const store = await db.getFromIndex("stores", "by-name-and-type", [
info.name, info.name,
info.type, info.type,
@ -183,6 +195,7 @@ export async function updateStore(info: StoreInfo, data: object | null) {
} }
export async function hasStore(info: StoreInfo) { export async function hasStore(info: StoreInfo) {
const db = await getDB();
const store = await db.getFromIndex("stores", "by-name-and-type", [ const store = await db.getFromIndex("stores", "by-name-and-type", [
info.name, info.name,
info.type, info.type,
@ -217,6 +230,7 @@ export function stores<T extends object>(
const customEvent = event as CustomEvent; const customEvent = event as CustomEvent;
const updatedStore = customEvent.detail as Store; const updatedStore = customEvent.detail as Store;
if (updatedStore.type === type) { if (updatedStore.type === type) {
const db = await getDB();
const stores = await db.getAllFromIndex("stores", "by-type", type); const stores = await db.getAllFromIndex("stores", "by-type", type);
state.splice( state.splice(
0, 0,
@ -228,6 +242,7 @@ export function stores<T extends object>(
} }
}); });
(async () => { (async () => {
const db = await getDB();
const stores = await db.getAllFromIndex("stores", "by-type", type); const stores = await db.getAllFromIndex("stores", "by-type", type);
state.splice( state.splice(
0, 0,

View File

@ -1,28 +1,15 @@
import { mount } from "svelte"; import { mount } from "svelte";
import "./app.css"; import "./app.css";
import App from "./App.svelte"; import App from "./App.svelte";
import OIDCCallback from "./OIDCCallback.svelte";
import { trySync } from "$lib/services/stores.svelte"; import { trySync } from "$lib/services/stores.svelte";
if (location.href.includes("/oidc")) { const app = mount(location.href.includes("/oidc") ? OIDCCallback : App, {
const url = new URL(location.href);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
if (code && state) {
window.opener.postMessage(
{
code,
state,
},
window.location.origin,
);
window.close();
}
}
const app = mount(App, {
target: document.getElementById("app")!, target: document.getElementById("app")!,
}); });
await trySync(); (async () => {
await trySync();
})();
export default app; export default app;