aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/bun.lock73
-rw-r--r--app/package.json16
-rw-r--r--app/src/lib/server/actual.ts20
-rw-r--r--app/src/lib/server/db/schema.ts27
-rw-r--r--app/src/lib/server/sb1.ts158
-rw-r--r--app/src/lib/server/session-log.ts4
-rw-r--r--app/src/lib/shared.ts141
-rw-r--r--app/src/routes/+page.svelte7
-rw-r--r--app/src/routes/methods.remote.ts24
-rw-r--r--app/src/routes/sb1-authorize/+server.ts21
-rw-r--r--app/src/routes/status.svelte17
11 files changed, 326 insertions, 182 deletions
diff --git a/app/bun.lock b/app/bun.lock
index 4a99872..887c58d 100644
--- a/app/bun.lock
+++ b/app/bun.lock
@@ -4,31 +4,30 @@
"": {
"name": "app",
"dependencies": {
- "pg": "^8.16.3",
- "pg-boss": "^12.5.4",
+ "pg": "latest",
},
"devDependencies": {
- "@actual-app/api": "^25.12.0",
- "@sveltejs/adapter-node": "^5.4.0",
- "@sveltejs/kit": "^2.49.2",
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
- "@types/node": "^25.0.3",
- "@types/pg": "^8.16.0",
- "drizzle-kit": "^0.31.8",
- "drizzle-orm": "^0.45.1",
- "lightningcss": "^1.30.2",
- "phosphor-svelte": "^3.0.1",
- "svelte": "^5.46.0",
- "svelte-check": "^4.3.5",
- "temporal-polyfill": "^0.3.0",
- "typescript": "^5.9.3",
- "valibot": "^1.2.0",
- "vite": "^7.3.0",
+ "@actual-app/api": "latest",
+ "@sveltejs/adapter-node": "latest",
+ "@sveltejs/kit": "latest",
+ "@sveltejs/vite-plugin-svelte": "latest",
+ "@types/node": "latest",
+ "@types/pg": "latest",
+ "drizzle-kit": "latest",
+ "drizzle-orm": "latest",
+ "lightningcss": "latest",
+ "phosphor-svelte": "latest",
+ "svelte": "latest",
+ "svelte-check": "latest",
+ "temporal-polyfill": "latest",
+ "typescript": "latest",
+ "valibot": "latest",
+ "vite": "latest",
},
},
},
"packages": {
- "@actual-app/api": ["@actual-app/api@25.12.0", "https://npm.ivar.systems/@actual-app/api/-/api-25.12.0.tgz", { "dependencies": { "@actual-app/crdt": "^2.1.0", "better-sqlite3": "^12.4.1", "compare-versions": "^6.1.1", "node-fetch": "^3.3.2", "uuid": "^13.0.0" } }, "sha512-W9xFQtHdEehEX7V6qKmOsaToK/Vj/Y/J11IHGKtowma5olhBF2qCCJcB6AKaHBmIZ6A3lkVd+WFkNfP5I/hREA=="],
+ "@actual-app/api": ["@actual-app/api@26.1.0", "https://npm.ivar.systems/@actual-app/api/-/api-26.1.0.tgz", { "dependencies": { "@actual-app/crdt": "^2.1.0", "better-sqlite3": "^12.4.1", "compare-versions": "^6.1.1", "node-fetch": "^3.3.2", "uuid": "^13.0.0" } }, "sha512-AXAQ93++XeCZdVU/wkxellxeehNfL0Zx7kSEDsW84nSVInuqmo+mlk5oi+fRpuXAcITLpkk9YcznYhKI7HfExg=="],
"@actual-app/crdt": ["@actual-app/crdt@2.1.0", "https://npm.ivar.systems/@actual-app/crdt/-/crdt-2.1.0.tgz", { "dependencies": { "google-protobuf": "^3.12.0-rc.1", "murmurhash": "^2.0.1", "uuid": "^9.0.0" } }, "sha512-Qb8hMq10Wi2kYIDj0fG4uy00f9Mloghd+xQrHQiPQfgx022VPJ/No+z/bmfj4MuFH8FrPiLysSzRsj2PNQIedw=="],
@@ -158,11 +157,11 @@
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "https://npm.ivar.systems/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
- "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.4.0", "https://npm.ivar.systems/@sveltejs/adapter-node/-/adapter-node-5.4.0.tgz", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ=="],
+ "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.0", "https://npm.ivar.systems/@sveltejs/adapter-node/-/adapter-node-5.5.0.tgz", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-xHzWyo2vRYqR/DyyFboIOVplz411RAyZvt0/UVPebRIhg3PGXty09mjiRt0nPj7zL0oPxqeCTu4RmHdsFkP/7w=="],
- "@sveltejs/kit": ["@sveltejs/kit@2.49.2", "https://npm.ivar.systems/@sveltejs/kit/-/kit-2.49.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ=="],
+ "@sveltejs/kit": ["@sveltejs/kit@2.49.4", "https://npm.ivar.systems/@sveltejs/kit/-/kit-2.49.4.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg=="],
- "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "https://npm.ivar.systems/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
+ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "https://npm.ivar.systems/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "https://npm.ivar.systems/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
@@ -170,7 +169,7 @@
"@types/estree": ["@types/estree@1.0.8", "https://npm.ivar.systems/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
- "@types/node": ["@types/node@25.0.3", "https://npm.ivar.systems/@types/node/-/node-25.0.3.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
+ "@types/node": ["@types/node@25.0.8", "https://npm.ivar.systems/@types/node/-/node-25.0.8.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
"@types/pg": ["@types/pg@8.16.0", "https://npm.ivar.systems/@types/pg/-/pg-8.16.0.tgz", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
@@ -206,8 +205,6 @@
"cookie": ["cookie@0.6.0", "https://npm.ivar.systems/cookie/-/cookie-0.6.0.tgz", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
- "cron-parser": ["cron-parser@5.4.0", "https://npm.ivar.systems/cron-parser/-/cron-parser-5.4.0.tgz", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA=="],
-
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "https://npm.ivar.systems/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@4.4.3", "https://npm.ivar.systems/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -302,8 +299,6 @@
"locate-character": ["locate-character@3.0.0", "https://npm.ivar.systems/locate-character/-/locate-character-3.0.0.tgz", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
- "luxon": ["luxon@3.7.2", "https://npm.ivar.systems/luxon/-/luxon-3.7.2.tgz", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
-
"magic-string": ["magic-string@0.30.21", "https://npm.ivar.systems/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mimic-response": ["mimic-response@3.1.0", "https://npm.ivar.systems/mimic-response/-/mimic-response-3.1.0.tgz", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
@@ -330,21 +325,21 @@
"node-fetch": ["node-fetch@3.3.2", "https://npm.ivar.systems/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+ "obug": ["obug@2.1.1", "https://npm.ivar.systems/obug/-/obug-2.1.1.tgz", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
"once": ["once@1.4.0", "https://npm.ivar.systems/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"path-parse": ["path-parse@1.0.7", "https://npm.ivar.systems/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
- "pg": ["pg@8.16.3", "https://npm.ivar.systems/pg/-/pg-8.16.3.tgz", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
-
- "pg-boss": ["pg-boss@12.5.4", "https://npm.ivar.systems/pg-boss/-/pg-boss-12.5.4.tgz", { "dependencies": { "cron-parser": "^5.4.0", "pg": "^8.16.3", "serialize-error": "^12.0.0" } }, "sha512-I52Gkpda6lLNZNDZVKN5sWYO2YKndI3bBwOGAHY7xC+UBYMHI3m2tWSmDGjuq/odf6zpGoFSKVUUukTEaBZocA=="],
+ "pg": ["pg@8.17.0", "https://npm.ivar.systems/pg/-/pg-8.17.0.tgz", { "dependencies": { "pg-connection-string": "^2.10.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-SRl6PbO7zqhD5bZ6lVtEFWknVeWv6Eab+PKzowWTEAce5MFiHTcSdi2N9M9m7VYAv1Hc3OOCxSka3hPBYoXHJA=="],
- "pg-cloudflare": ["pg-cloudflare@1.2.7", "https://npm.ivar.systems/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
+ "pg-cloudflare": ["pg-cloudflare@1.3.0", "https://npm.ivar.systems/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
- "pg-connection-string": ["pg-connection-string@2.9.1", "https://npm.ivar.systems/pg-connection-string/-/pg-connection-string-2.9.1.tgz", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
+ "pg-connection-string": ["pg-connection-string@2.10.0", "https://npm.ivar.systems/pg-connection-string/-/pg-connection-string-2.10.0.tgz", {}, "sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg=="],
"pg-int8": ["pg-int8@1.0.1", "https://npm.ivar.systems/pg-int8/-/pg-int8-1.0.1.tgz", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
- "pg-pool": ["pg-pool@3.10.1", "https://npm.ivar.systems/pg-pool/-/pg-pool-3.10.1.tgz", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
+ "pg-pool": ["pg-pool@3.11.0", "https://npm.ivar.systems/pg-pool/-/pg-pool-3.11.0.tgz", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
"pg-protocol": ["pg-protocol@1.10.3", "https://npm.ivar.systems/pg-protocol/-/pg-protocol-1.10.3.tgz", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
@@ -390,8 +385,6 @@
"semver": ["semver@7.7.3", "https://npm.ivar.systems/semver/-/semver-7.7.3.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
- "serialize-error": ["serialize-error@12.0.0", "https://npm.ivar.systems/serialize-error/-/serialize-error-12.0.0.tgz", { "dependencies": { "type-fest": "^4.31.0" } }, "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw=="],
-
"set-cookie-parser": ["set-cookie-parser@2.7.2", "https://npm.ivar.systems/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"simple-concat": ["simple-concat@1.0.1", "https://npm.ivar.systems/simple-concat/-/simple-concat-1.0.1.tgz", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
@@ -414,7 +407,7 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://npm.ivar.systems/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
- "svelte": ["svelte@5.46.0", "https://npm.ivar.systems/svelte/-/svelte-5.46.0.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="],
+ "svelte": ["svelte@5.46.3", "https://npm.ivar.systems/svelte/-/svelte-5.46.3.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Y5juST3x+/ySty5tYJCVWa6Corkxpt25bUZQHqOceg9xfMUtDsFx6rCsG6cYf1cA6vzDi66HIvaki0byZZX95A=="],
"svelte-check": ["svelte-check@4.3.5", "https://npm.ivar.systems/svelte-check/-/svelte-check-4.3.5.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
@@ -432,8 +425,6 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "https://npm.ivar.systems/tunnel-agent/-/tunnel-agent-0.6.0.tgz", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
- "type-fest": ["type-fest@4.41.0", "https://npm.ivar.systems/type-fest/-/type-fest-4.41.0.tgz", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
-
"typescript": ["typescript@5.9.3", "https://npm.ivar.systems/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "https://npm.ivar.systems/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
@@ -444,7 +435,7 @@
"valibot": ["valibot@1.2.0", "https://npm.ivar.systems/valibot/-/valibot-1.2.0.tgz", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
- "vite": ["vite@7.3.0", "https://npm.ivar.systems/vite/-/vite-7.3.0.tgz", { "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-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
+ "vite": ["vite@7.3.1", "https://npm.ivar.systems/vite/-/vite-7.3.1.tgz", { "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=="],
"vitefu": ["vitefu@1.1.1", "https://npm.ivar.systems/vitefu/-/vitefu-1.1.1.tgz", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
@@ -466,6 +457,10 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "https://npm.ivar.systems/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "@types/pg/@types/node": ["@types/node@25.0.3", "https://npm.ivar.systems/@types/node/-/node-25.0.3.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
+
+ "pg/pg-protocol": ["pg-protocol@1.11.0", "https://npm.ivar.systems/pg-protocol/-/pg-protocol-1.11.0.tgz", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
+
"vite/esbuild": ["esbuild@0.27.2", "https://npm.ivar.systems/esbuild/-/esbuild-0.27.2.tgz", { "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=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "https://npm.ivar.systems/@esbuild/android-arm/-/android-arm-0.18.20.tgz", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
diff --git a/app/package.json b/app/package.json
index 1de0412..4c449c4 100644
--- a/app/package.json
+++ b/app/package.json
@@ -16,24 +16,24 @@
"db:studio": "drizzle-kit studio"
},
"devDependencies": {
- "@actual-app/api": "^25.12.0",
- "@sveltejs/adapter-node": "^5.4.0",
- "@sveltejs/kit": "^2.49.2",
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
- "@types/node": "^25.0.3",
+ "@actual-app/api": "^26.1.0",
+ "@sveltejs/adapter-node": "^5.5.0",
+ "@sveltejs/kit": "^2.49.4",
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@types/node": "^25.0.8",
"@types/pg": "^8.16.0",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"lightningcss": "^1.30.2",
"phosphor-svelte": "^3.0.1",
- "svelte": "^5.46.0",
+ "svelte": "^5.46.3",
"svelte-check": "^4.3.5",
"temporal-polyfill": "^0.3.0",
"typescript": "^5.9.3",
"valibot": "^1.2.0",
- "vite": "^7.3.0"
+ "vite": "^7.3.1"
},
"dependencies": {
- "pg": "^8.16.3"
+ "pg": "^8.17.0"
}
} \ No newline at end of file
diff --git a/app/src/lib/server/actual.ts b/app/src/lib/server/actual.ts
index 4cf0262..fb1a4fb 100644
--- a/app/src/lib/server/actual.ts
+++ b/app/src/lib/server/actual.ts
@@ -1,13 +1,13 @@
-import { ACTUAL_BUDGET_ID, ACTUAL_HOST, ACTUAL_PASS } from "$env/static/private";
+import { ACTUAL_FILE_ID, ACTUAL_HOST, ACTUAL_PASS } from "$env/static/private";
import * as actual from "@actual-app/api"
import { existsSync, mkdirSync } from "node:fs";
import path from "node:path"
import process from "node:process";
-import type { Sb1Transaction } from "./sb1";
import type { ImportTransactionEntity } from "@actual-app/api/@types/loot-core/src/types/models/import-transaction";
import { Temporal } from "temporal-polyfill";
+import type { Sb1Transaction } from "$lib/shared";
-async function init_actual() {
+export async function init_actual() {
const dataDir = path.resolve(process.cwd(), "data/actualDataDir")
if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
return actual.init({
@@ -15,33 +15,37 @@ async function init_actual() {
serverURL: ACTUAL_HOST,
dataDir: dataDir
}).then(async () => {
- await actual.downloadBudget(ACTUAL_BUDGET_ID)
+ await actual.downloadBudget(ACTUAL_FILE_ID)
await actual.sync()
})
}
export async function import_transactions(account: string, transactions: Sb1Transaction[], dryRun: boolean) {
-
+ await init_actual()
function parsedDate(date: number) {
const instant = Temporal.Instant.fromEpochMilliseconds(date)
return instant.toString({ timeZone: "Europe/Oslo" }).split("T")[0]
}
function notes(transaction: Sb1Transaction) {
- const {description,cleanedDescription} =transaction
+ const { description, cleanedDescription } = transaction
if (description.toLowerCase().trim() === cleanedDescription.toLowerCase().trim()) return undefined
return description
}
+ function amount(amount: number) {
+ return Math.round(amount * 100)
+ }
+
const actualMappedTransactions: ImportTransactionEntity[] = transactions.filter(c => c.bookingStatus === "BOOKED").map(c => ({
account,
date: parsedDate(c.date),
- amount: c.amount,
+ amount: amount(c.amount),
notes: notes(c),
payee_name: c.cleanedDescription
}))
- return await actual.importTransactions(account, actualMappedTransactions, { dryRun })
+ await actual.importTransactions(account, actualMappedTransactions, { dryRun })
}
export async function get_budgets() {
diff --git a/app/src/lib/server/db/schema.ts b/app/src/lib/server/db/schema.ts
index 4d13ed6..dbff3a2 100644
--- a/app/src/lib/server/db/schema.ts
+++ b/app/src/lib/server/db/schema.ts
@@ -1,17 +1,17 @@
import { relations, sql } from 'drizzle-orm';
-import { numeric, text, pgTable, uuid, json } from "drizzle-orm/pg-core";
-import type { Sb1Tokens } from '../sb1';
+import { numeric, text, pgTable, uuid, jsonb } from "drizzle-orm/pg-core";
import type { SessionLogType } from '../session-log';
+import type { Sb1Tokens, Sb1Transaction, Sb1TransactionDetails } from '$lib/shared';
-export const syncSession = pgTable("session", {
+export const SyncSessionTable = pgTable("session", {
id: uuid('id').primaryKey().default(sql`uuidv7()`),
authzState: text("authzState"),
accessTokenCreated: numeric("accessTokenCreated"),
refreshTokenCreated: numeric("refreshTokenCreated"),
- tokens: json("tokens").$type<Sb1Tokens>()
+ tokens: jsonb("tokens").$type<Sb1Tokens>()
})
-export const syncLog = pgTable("session_log", {
+export const SyncLogTable = pgTable("session_log", {
id: uuid('id').primaryKey().default(sql`uuidv7()`),
sessionId: text("session_id"),
dateTime: text("date_time"),
@@ -19,13 +19,18 @@ export const syncLog = pgTable("session_log", {
msg: text("msg")
})
-export const syncLogRelation = relations(syncLog, ({ one }) => ({
- author: one(syncSession, {
- fields: [syncLog.sessionId],
- references: [syncSession.id],
+export const TransactionsTable = pgTable("transactions", {
+ transaction: jsonb("transaction").$type<Sb1Transaction>(),
+ details: jsonb("details").$type<Sb1TransactionDetails>()
+})
+
+export const SyncLogRelation = relations(SyncLogTable, ({ one }) => ({
+ author: one(SyncSessionTable, {
+ fields: [SyncLogTable.sessionId],
+ references: [SyncSessionTable.id],
})
}))
-export const syncSessionLogRelation = relations(syncSession, ({ many }) => ({
- logs: many(syncLog)
+export const SyncSessionLogRelation = relations(SyncSessionTable, ({ many }) => ({
+ logs: many(SyncLogTable)
}))
diff --git a/app/src/lib/server/sb1.ts b/app/src/lib/server/sb1.ts
index f6507ef..0a51649 100644
--- a/app/src/lib/server/sb1.ts
+++ b/app/src/lib/server/sb1.ts
@@ -1,84 +1,11 @@
import { SB1_FIN_INST, SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from "$env/static/private";
-import { eq } from "drizzle-orm";
+import { eq, sql } from "drizzle-orm";
import { Temporal } from "temporal-polyfill";
import { randomUUID } from "node:crypto";
import { db } from "./db";
-import { syncSession } from "./db/schema";
+import { SyncSessionTable, TransactionsTable } from "./db/schema";
import { add_session_log } from "./session-log";
-
-export type Sb1Tokens = {
- access_token: string
- expires_in: number
- refresh_token_expires_in: number
- refresh_token_absolute_expires_in: number
- token_type: string
- refresh_token: string
-}
-
-export type Sb1Transaction = {
- id: string
- nonUniqueId: string
- description: string
- cleanedDescription: string
- accountNumber: Sb1AccountNumber
- amount: number
- date: number
- interestDate: number
- typeCode: string
- typeText: string
- currencyCode: string
- canShowDetails: boolean
- source: string
- isConfidential: boolean
- bookingStatus: string
- accountName: string
- accountKey: string
- accountCurrency: string
- isFromCurrencyAccount: boolean
- classificationInput: Sb1ClassificationInput
-}
-
-export type Sb1Account = {
- key: string;
- accountNumber: string;
- iban: string;
- name: string;
- description: string;
- balance: number;
- availableBalance: number;
- currencyCode: string;
- owner: Sb1AccountOwner;
- productType: string;
- type: string;
- productId: string;
- descriptionCode: string;
- accountProperties: { [key: string]: boolean };
-}
-
-export type Sb1AccountOwner = {
- name: string;
- firstName: string;
- lastName: string;
- type: string;
- age: number;
- customerKey: string;
- ssnKey: string;
-}
-
-
-export type Sb1AccountNumber = {
- value: string
- formatted: string
- unformatted: string
-}
-
-export type Sb1ClassificationInput = {
- id: string
- amount: number
- type: string
- text: string
- date: string
-}
+import type { Sb1Account, Sb1Tokens, Sb1Transaction } from "$lib/shared";
const auth = {
async is_ready(): Promise<boolean> {
@@ -87,10 +14,10 @@ const auth = {
},
async get_auth_info() {
const entity = await db.select({
- refreshTokenCreated: syncSession.refreshTokenCreated,
- accessTokenCreated: syncSession.accessTokenCreated,
- tokens: syncSession.tokens
- }).from(syncSession)
+ refreshTokenCreated: SyncSessionTable.refreshTokenCreated,
+ accessTokenCreated: SyncSessionTable.accessTokenCreated,
+ tokens: SyncSessionTable.tokens
+ }).from(SyncSessionTable)
if (!entity[0]) return undefined
const { tokens, accessTokenCreated, refreshTokenCreated } = entity[0]
if (!tokens) return undefined
@@ -104,7 +31,7 @@ const auth = {
async init_auth_session(): Promise<string> {
const state = randomUUID()
- await db.insert(syncSession).values({
+ await db.insert(SyncSessionTable).values({
authzState: state
})
@@ -118,19 +45,25 @@ const auth = {
},
async get_access_token() {
const result = await db.select({
- tokens: syncSession.tokens,
- refreshTokenCreated: syncSession.refreshTokenCreated,
- accessTokenCreated: syncSession.accessTokenCreated
- }).from(syncSession)
+ tokens: SyncSessionTable.tokens,
+ refreshTokenCreated: SyncSessionTable.refreshTokenCreated,
+ accessTokenCreated: SyncSessionTable.accessTokenCreated
+ }).from(SyncSessionTable)
+
if (!result[0]) return undefined
+
const { tokens, accessTokenCreated, refreshTokenCreated } = result[0]
+
if (!tokens) return undefined
+
const nowInstant = Temporal.Now.instant()
+
const accessTokenExpiredInstant = Temporal.Instant.fromEpochMilliseconds(Number(accessTokenCreated)).add({ seconds: tokens.expires_in })
if (Temporal.Instant.compare(nowInstant, accessTokenExpiredInstant) >= 0) {
const refreshedTokens = await this.refresh_token()
if (refreshedTokens) return refreshedTokens.access_token
}
+
const refreshTokenExpiredInstant = Temporal.Instant.fromEpochMilliseconds(Number(refreshTokenCreated)).add({ seconds: tokens.refresh_token_expires_in })
if (Temporal.Instant.compare(nowInstant, refreshTokenExpiredInstant) >= 0) {
return undefined
@@ -141,9 +74,9 @@ const auth = {
async refresh_token(): Promise<Sb1Tokens | null> {
console.log("Refreshing tokens")
const entity = await db.select({
- tokens: syncSession.tokens,
- id: syncSession.id
- }).from(syncSession)
+ tokens: SyncSessionTable.tokens,
+ id: SyncSessionTable.id
+ }).from(SyncSessionTable)
const { tokens: currentTokens, id } = entity[0]
@@ -167,7 +100,7 @@ const auth = {
const tokens = await res.json() as Sb1Tokens
const epoch = Temporal.Now.instant().epochMilliseconds
if (res.ok) {
- await db.update(syncSession).set({ tokens, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(syncSession.id, id))
+ await db.update(SyncSessionTable).set({ tokens, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(SyncSessionTable.id, id))
await add_session_log(id, "REFRESH_SB1_TOKEN", "Done")
return tokens
} else {
@@ -195,25 +128,60 @@ const data = {
},
async get_transactions(accountKey: string, delta?: Temporal.Instant) {
const token = await auth.get_access_token()
-
if (!token) return undefined
+
const params = new URLSearchParams({
"accountKey": accountKey,
});
- if (delta) params.append("fromDate", formatInstant(delta, "yyyy-MM-dd"))
+
+ if (delta) {
+ params.append("fromDate", formatInstant(delta, "yyyy-MM-dd"))
+ params.append("Transaction source", "ALL")
+ }
+
const response = await fetch("https://api.sparebank1.no/personal/banking/transactions?" + params, {
headers: {
Authorization: `Bearer ${token}`,
+ Accept: "application/vnd.sparebank1.v1+json;charset=utf-8"
},
});
+
const json = await response.json()
- return json["transactions"] as Sb1Transaction[];
+ return json["transactions"] as Sb1Transaction[]
}
}
-export default { auth, data }
+export default { auth, data, init }
-export function formatInstant(
+let importInterval: NodeJS.Timeout
+let inited = false
+
+async function init() {
+ if (inited) return
+ if (importInterval) clearInterval(importInterval)
+ await importTransactions()
+ importInterval = setInterval(async () => importTransactions, 60 * 60 * 1000)
+ inited = true
+}
+
+async function importTransactions() {
+ console.log("Creating sb1 transactions indb")
+ const accounts = await data.get_accounts()
+ for (const account of accounts?.accounts ?? []) {
+ const transactions = await data.get_transactions(account.key)
+ for (const transaction of transactions ?? []) {
+ // if (await transactionExists(transaction.id)) continue
+ await db.insert(TransactionsTable).values({ transaction })
+ }
+ }
+}
+
+async function transactionExists(transactionId: string) {
+ const query = sql`select data ->>'id' as id from ${TransactionsTable} where id=${transactionId}`;
+ return (await db.execute(query)).rowCount ?? 0 > 0
+}
+
+function formatInstant(
instant: Temporal.Instant,
format: string,
timeZone: string = "UTC"
@@ -236,4 +204,4 @@ export function formatInstant(
/yyyy|MM|dd|HH|mm|ss/g,
(token) => replacements[token]
);
-}
+} \ No newline at end of file
diff --git a/app/src/lib/server/session-log.ts b/app/src/lib/server/session-log.ts
index 1195621..54498f3 100644
--- a/app/src/lib/server/session-log.ts
+++ b/app/src/lib/server/session-log.ts
@@ -1,11 +1,11 @@
import { Temporal } from "temporal-polyfill"
import { db } from "./db"
-import { syncLog } from "./db/schema"
+import { SyncLogTable } from "./db/schema"
export type SessionLogType = "CREATED" | "SYNC_START" | "REFRESH_SB1_TOKEN"
export async function add_session_log(id: string, type: SessionLogType, msg: string) {
- db.insert(syncLog).values({
+ db.insert(SyncLogTable).values({
dateTime: String(Temporal.Now.instant().epochMilliseconds),
sessionId: id,
type: type,
diff --git a/app/src/lib/shared.ts b/app/src/lib/shared.ts
index a7cf207..cc4472f 100644
--- a/app/src/lib/shared.ts
+++ b/app/src/lib/shared.ts
@@ -11,3 +11,144 @@ export const ImportForm = v.object({
),
dryRun: v.boolean()
})
+export type Sb1TransactionDetails = {
+ id: string;
+ date: Date;
+ type: string;
+ amount: number;
+ typeCode: string;
+ typeText: string;
+ valueDate: Date;
+ accountKey: string;
+ bookedDate: Date;
+ accountName: string;
+ description: string;
+ eInvoiceUrl: string;
+ nonUniqueId: string;
+ postingDate: Date;
+ currencyCode: string;
+ exchangeRate: number;
+ kidOrMessage: string;
+ accountNumber: number;
+ currencyAmount: number;
+ paymentDetails: Sb1PaymentDetails;
+ accountCurrency: string;
+ archiveReference: string;
+ paymentReference: string;
+ remoteAccountName: string;
+ cleanedDescription: string;
+ numericalReference: string;
+ classificationInput: Sb1ClassificationInput;
+ originalDescription: string;
+ remoteAccountNumber: string;
+}
+
+export type Sb1PaymentDetails = {
+ amount: number;
+ message: string;
+ paymentCid: string;
+ payeeAddress: Sb1PayeeAddress;
+ payeeBankName: string;
+ payeeBicSwift: string;
+ amountCurrency: string;
+ serviceCharges: Sb1ServiceCharge[];
+ payeeBankAddress: Sb1PayeeAddress;
+ paymentReference: string;
+ payeeEmailAddress: string;
+ internationalDetails: Sb1InternationalDetails;
+}
+
+export type Sb1InternationalDetails = {
+ agreedRate: string;
+ agreedWith: string;
+ authorityReportCode: string;
+ authorityReportText: string;
+}
+
+export type Sb1PayeeAddress = {
+ city: string;
+ line1: string;
+ line2: string;
+ line3: string;
+ zipCode: string;
+ countryCode: string;
+}
+
+export type Sb1ServiceCharge = {
+ paidBy: string;
+ chargedAmount: number;
+ chargedAmountCurrency: string;
+}
+
+export type Sb1Tokens = {
+ access_token: string
+ expires_in: number
+ refresh_token_expires_in: number
+ refresh_token_absolute_expires_in: number
+ token_type: string
+ refresh_token: string
+}
+
+export type Sb1Transaction = {
+ id: string
+ nonUniqueId: string
+ description: string
+ cleanedDescription: string
+ accountNumber: Sb1AccountNumber
+ amount: number
+ date: number
+ interestDate: number
+ typeCode: string
+ typeText: string
+ currencyCode: string
+ canShowDetails: boolean
+ source: string
+ isConfidential: boolean
+ bookingStatus: string
+ accountName: string
+ accountKey: string
+ accountCurrency: string
+ isFromCurrencyAccount: boolean
+ classificationInput: Sb1ClassificationInput
+}
+
+export type Sb1Account = {
+ key: string;
+ accountNumber: string;
+ iban: string;
+ name: string;
+ description: string;
+ balance: number;
+ availableBalance: number;
+ currencyCode: string;
+ owner: Sb1AccountOwner;
+ productType: string;
+ type: string;
+ productId: string;
+ descriptionCode: string;
+ accountProperties: { [key: string]: boolean };
+}
+
+export type Sb1AccountOwner = {
+ name: string;
+ firstName: string;
+ lastName: string;
+ type: string;
+ age: number;
+ customerKey: string;
+ ssnKey: string;
+}
+
+export type Sb1AccountNumber = {
+ value: string
+ formatted: string
+ unformatted: string
+}
+
+export type Sb1ClassificationInput = {
+ id: string
+ amount: number
+ type: string
+ text: string
+ date: string
+}
diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte
index ee4148b..b12c471 100644
--- a/app/src/routes/+page.svelte
+++ b/app/src/routes/+page.svelte
@@ -12,7 +12,8 @@
dryRun: true,
});
- async function run() {
+ async function run(e: SubmitEvent) {
+ e.preventDefault();
if (!form.mappings.length) {
return;
}
@@ -21,8 +22,7 @@
async function authorize() {
navigating = true;
- const url = await init_auth_session();
- location.href = url;
+ location.href = await init_auth_session();
}
async function logout() {
@@ -74,6 +74,7 @@
</form>
<h3>Annet</h3>
<Button onclick={logout} loading={navigating}>Logg ut</Button>
+ <div></div>
{:else}
<Button onclick={authorize} loading={navigating}>Autentisér hos Sparebanken 1</Button>
{/if}
diff --git a/app/src/routes/methods.remote.ts b/app/src/routes/methods.remote.ts
index 3fca715..d9ba812 100644
--- a/app/src/routes/methods.remote.ts
+++ b/app/src/routes/methods.remote.ts
@@ -1,8 +1,8 @@
import { db } from "$lib/server/db";
-import { syncSession } from "$lib/server/db/schema";
+import { SyncSessionTable } from "$lib/server/db/schema";
import { command, query } from "$app/server";
import sb1 from "$lib/server/sb1";
-import { import_transactions } from "$lib/server/actual";
+import { import_transactions, init_actual } from "$lib/server/actual";
import { ImportForm } from "$lib/shared";
const init_auth_session = command(async () => {
@@ -10,21 +10,31 @@ const init_auth_session = command(async () => {
})
const clear_auth_session = query(async () => {
- await db.delete(syncSession)
+ await db.delete(SyncSessionTable)
})
const do_import = command(ImportForm, async (form) => {
- let x
for (const mapping of form.mappings) {
const transactions = await sb1.data.get_transactions(mapping.sb1Id)
- if (!transactions?.length || x) continue
- x = true
- console.log(await import_transactions(mapping.actualId, transactions, form.dryRun))
+ console.log(transactions)
+ continue
+ // if (!transactions?.length) continue
+ // console.log(await import_transactions(mapping.actualId, transactions, form.dryRun))
}
})
+const init_sb1 = command(async () => {
+ return await sb1.init()
+})
+
+const _init_actual = command(async () => {
+ return await init_actual()
+})
+
export {
init_auth_session,
do_import,
+ _init_actual as init_actual,
+ init_sb1,
clear_auth_session
}
diff --git a/app/src/routes/sb1-authorize/+server.ts b/app/src/routes/sb1-authorize/+server.ts
index b3a0cf7..d6b8fbf 100644
--- a/app/src/routes/sb1-authorize/+server.ts
+++ b/app/src/routes/sb1-authorize/+server.ts
@@ -1,10 +1,11 @@
-import { error, redirect } from '@sveltejs/kit';
+import { error, redirect, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
-import { syncSession } from '$lib/server/db/schema';
+import { SyncSessionTable } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from '$env/static/private';
import { Temporal } from "temporal-polyfill"
+import sb1 from "$lib/server/sb1"
export const GET: RequestHandler = async ({ url }) => {
const code = url.searchParams.get('code')
@@ -13,16 +14,19 @@ export const GET: RequestHandler = async ({ url }) => {
if (!code) error(400, "?code is missing")
if (!state) error(400, "?state is missing")
- const session = await db.select().from(syncSession).where(eq(syncSession.authzState, state))
+ const session = await db.select().from(SyncSessionTable).where(eq(SyncSessionTable.authzState, state))
const { id } = session[0]
+ if (!id) return error(500, "Ingen session")
const fd = new URLSearchParams()
+
fd.set("client_id", SB1_ID)
fd.set("client_secret", SB1_SECRET)
fd.set("redirect_uri", SB1_REDIRECT_URI)
fd.set("code", code)
fd.set("state", state)
fd.set("grant_type", "authorization_code")
+
const response = await fetch("https://api.sparebank1.no/oauth/token", {
method: "post",
headers: {
@@ -31,15 +35,14 @@ export const GET: RequestHandler = async ({ url }) => {
body: fd
})
- const json = await response.json()
+ const responseJson = await response.json()
if (response.ok) {
const epoch = Temporal.Now.instant().epochMilliseconds
- await db.update(syncSession).set({ tokens: json, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(syncSession.id, id))
+ await db.update(SyncSessionTable).set({ tokens: responseJson, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(SyncSessionTable.id, id))
+ await sb1.init()
redirect(302, "/")
} else {
- return new Response(json)
+ return json(responseJson)
}
-
- return new Response()
-} \ No newline at end of file
+}
diff --git a/app/src/routes/status.svelte b/app/src/routes/status.svelte
new file mode 100644
index 0000000..fe09193
--- /dev/null
+++ b/app/src/routes/status.svelte
@@ -0,0 +1,17 @@
+<script lang="ts">
+ type Props = {
+ type: "sb1" | "actual";
+ };
+ import { onMount } from "svelte";
+ let { type }: Props = $props();
+
+ onMount(() => {});
+</script>
+
+<div>
+ <span>{type}</span>
+ <div class="status"></div>
+ <div class="refresh">&#10226;</div>
+</div>
+
+<style></style>