feat: improve OIDC login flow and stores handling
This commit is contained in:
35
src/OIDCCallback.svelte
Normal file
35
src/OIDCCallback.svelte
Normal 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>
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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,20 +44,26 @@ interface TCDB extends DBSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = await openDB<TCDB>("tc", 1, {
|
export async function getDB(): Promise<IDBPDatabase<TCDB>> {
|
||||||
upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
|
if (_db) return _db;
|
||||||
if (!db.objectStoreNames.contains("stores")) {
|
_db = await openDB<TCDB>("tc", 1, {
|
||||||
const store = db.createObjectStore("stores", { keyPath: "id" });
|
upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
|
||||||
store.createIndex("by-type", "type");
|
if (!db.objectStoreNames.contains("stores")) {
|
||||||
store.createIndex("by-owner", "owner_id");
|
const store = db.createObjectStore("stores", { keyPath: "id" });
|
||||||
store.createIndex("by-type-and-owner", ["type", "owner_id"]);
|
store.createIndex("by-type", "type");
|
||||||
store.createIndex("by-name-and-type", ["name", "type"]);
|
store.createIndex("by-owner", "owner_id");
|
||||||
}
|
store.createIndex("by-type-and-owner", ["type", "owner_id"]);
|
||||||
if (!db.objectStoreNames.contains("changes")) {
|
store.createIndex("by-name-and-type", ["name", "type"]);
|
||||||
db.createObjectStore("changes", { keyPath: "id" });
|
}
|
||||||
}
|
if (!db.objectStoreNames.contains("changes")) {
|
||||||
},
|
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,
|
||||||
|
|||||||
23
src/main.ts
23
src/main.ts
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user