From b8b6e229281be13258715870ddd0c2e1866dee12 Mon Sep 17 00:00:00 2001 From: ivar Date: Mon, 22 Jun 2026 00:46:09 +0200 Subject: Remove webapp --- app/src/app.d.ts | 13 -- app/src/app.html | 14 --- app/src/lib/helpers.ts | 5 - app/src/lib/server/actual.ts | 72 ----------- app/src/lib/server/db/index.ts | 6 - app/src/lib/server/db/schema.ts | 36 ------ app/src/lib/server/sb1.ts | 207 -------------------------------- app/src/lib/server/session-log.ts | 14 --- app/src/lib/shared.ts | 154 ------------------------ app/src/lib/ui/button.svelte | 41 ------- app/src/routes/+page.server.ts | 15 --- app/src/routes/+page.svelte | 94 --------------- app/src/routes/methods.remote.ts | 38 ------ app/src/routes/sb1-authorize/+server.ts | 48 -------- app/src/routes/status.svelte | 17 --- 15 files changed, 774 deletions(-) delete mode 100644 app/src/app.d.ts delete mode 100644 app/src/app.html delete mode 100644 app/src/lib/helpers.ts delete mode 100644 app/src/lib/server/actual.ts delete mode 100644 app/src/lib/server/db/index.ts delete mode 100644 app/src/lib/server/db/schema.ts delete mode 100644 app/src/lib/server/sb1.ts delete mode 100644 app/src/lib/server/session-log.ts delete mode 100644 app/src/lib/shared.ts delete mode 100644 app/src/lib/ui/button.svelte delete mode 100644 app/src/routes/+page.server.ts delete mode 100644 app/src/routes/+page.svelte delete mode 100644 app/src/routes/methods.remote.ts delete mode 100644 app/src/routes/sb1-authorize/+server.ts delete mode 100644 app/src/routes/status.svelte (limited to 'app/src') diff --git a/app/src/app.d.ts b/app/src/app.d.ts deleted file mode 100644 index da08e6d..0000000 --- a/app/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/app/src/app.html b/app/src/app.html deleted file mode 100644 index 91f0602..0000000 --- a/app/src/app.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - %sveltekit.head% - - - -
%sveltekit.body%
- - - \ No newline at end of file diff --git a/app/src/lib/helpers.ts b/app/src/lib/helpers.ts deleted file mode 100644 index 35b5f65..0000000 --- a/app/src/lib/helpers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Temporal } from "temporal-polyfill"; - -export function instantAsHtmlInputValueString(instant: Temporal.Instant) { - return instant.toString().split("T")[0] -} \ No newline at end of file diff --git a/app/src/lib/server/actual.ts b/app/src/lib/server/actual.ts deleted file mode 100644 index ca4d9c4..0000000 --- a/app/src/lib/server/actual.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ACTUAL_FILE_ID, ACTUAL_HOST, ACTUAL_PASS } from "$env/static/private"; -import * as actualApi from "@actual-app/api" -import { existsSync, mkdirSync } from "node:fs"; -import path from "node:path" -import process from "node:process"; -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"; - -let inited = false - -async function init() { - if (inited) return - const dataDir = path.resolve(process.cwd(), "data/actualDataDir") - if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true }); - await actualApi.init({ - password: ACTUAL_PASS, - serverURL: ACTUAL_HOST, - dataDir - }) - await actualApi.downloadBudget(ACTUAL_FILE_ID) - await actualApi.sync() - inited = true -} - -const budget = { - async get_budgets() { - await init() - return actualApi.getBudgets() - }, - - async get_accounts() { - await init() - return actualApi.getAccounts() - }, - - async import_transactions(account: string, transactions: Sb1Transaction[], dryRun: boolean) { - await init() - - function parsedDate(date: number) { - return Temporal.Instant.fromEpochMilliseconds(date) - .toString({ timeZone: "Europe/Oslo" }) - .split("T")[0] - } - - function notes(transaction: Sb1Transaction) { - const { description, cleanedDescription } = transaction - if (description?.toLowerCase().trim() === cleanedDescription?.toLowerCase().trim()) return undefined - return description - } - - function amount(amount: number) { - const res = Math.round(amount * 10000) - console.log(`${amount}->${res}`) - return res - } - - const mapped: ImportTransactionEntity[] = transactions - .filter(c => c.bookingStatus === "BOOKED") - .map(c => ({ - account, - date: parsedDate(c.date), - amount: amount(c.amount), - notes: notes(c), - payee_name: c.cleanedDescription - })) - - return actualApi.importTransactions(account, mapped, { dryRun }) - } -} - -export default { init, budget } diff --git a/app/src/lib/server/db/index.ts b/app/src/lib/server/db/index.ts deleted file mode 100644 index e477388..0000000 --- a/app/src/lib/server/db/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { drizzle } from 'drizzle-orm/node-postgres'; -import { env } from '$env/dynamic/private'; - -if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); - -export const db = drizzle(env.DATABASE_URL); \ No newline at end of file diff --git a/app/src/lib/server/db/schema.ts b/app/src/lib/server/db/schema.ts deleted file mode 100644 index dbff3a2..0000000 --- a/app/src/lib/server/db/schema.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { relations, sql } from 'drizzle-orm'; -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 SyncSessionTable = pgTable("session", { - id: uuid('id').primaryKey().default(sql`uuidv7()`), - authzState: text("authzState"), - accessTokenCreated: numeric("accessTokenCreated"), - refreshTokenCreated: numeric("refreshTokenCreated"), - tokens: jsonb("tokens").$type() -}) - -export const SyncLogTable = pgTable("session_log", { - id: uuid('id').primaryKey().default(sql`uuidv7()`), - sessionId: text("session_id"), - dateTime: text("date_time"), - type: text("type").$type(), - msg: text("msg") -}) - -export const TransactionsTable = pgTable("transactions", { - transaction: jsonb("transaction").$type(), - details: jsonb("details").$type() -}) - -export const SyncLogRelation = relations(SyncLogTable, ({ one }) => ({ - author: one(SyncSessionTable, { - fields: [SyncLogTable.sessionId], - references: [SyncSessionTable.id], - }) -})) - -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 deleted file mode 100644 index 0a51649..0000000 --- a/app/src/lib/server/sb1.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { SB1_FIN_INST, SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from "$env/static/private"; -import { eq, sql } from "drizzle-orm"; -import { Temporal } from "temporal-polyfill"; -import { randomUUID } from "node:crypto"; -import { db } from "./db"; -import { SyncSessionTable, TransactionsTable } from "./db/schema"; -import { add_session_log } from "./session-log"; -import type { Sb1Account, Sb1Tokens, Sb1Transaction } from "$lib/shared"; - -const auth = { - async is_ready(): Promise { - const token = await this.get_access_token() - return token !== "" - }, - async get_auth_info() { - const entity = await db.select({ - 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 - const refreshTokenExpires = Temporal.Instant.fromEpochMilliseconds(Number(refreshTokenCreated)).add({ seconds: tokens.refresh_token_expires_in }) - const accessTokenExpires = Temporal.Instant.fromEpochMilliseconds(Number(accessTokenCreated)).add({ seconds: tokens.expires_in }) - return { - refreshTokenExpires, - accessTokenExpires - } - }, - async init_auth_session(): Promise { - const state = randomUUID() - - await db.insert(SyncSessionTable).values({ - authzState: state - }) - - const authorizeUrl = new URL("https://api.sparebank1.no/oauth/authorize"); - authorizeUrl.searchParams.set("client_id", SB1_ID); - authorizeUrl.searchParams.set("state", state); - authorizeUrl.searchParams.set("redirect_uri", SB1_REDIRECT_URI); - authorizeUrl.searchParams.set("finInst", SB1_FIN_INST); - authorizeUrl.searchParams.set("response_type", "code"); - return authorizeUrl.toString() - }, - async get_access_token() { - const result = await db.select({ - 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 - } - - return tokens?.access_token as string - }, - async refresh_token(): Promise { - console.log("Refreshing tokens") - const entity = await db.select({ - tokens: SyncSessionTable.tokens, - id: SyncSessionTable.id - }).from(SyncSessionTable) - - const { tokens: currentTokens, id } = entity[0] - - if (!currentTokens) return null - if (!currentTokens.refresh_token) throw new Error("No refresh token"); - - const params = new URLSearchParams(); - - params.set("client_id", SB1_ID); - params.set("client_secret", SB1_SECRET); - params.set("refresh_token", currentTokens.refresh_token); - params.set("grant_type", "refresh_token"); - - const res = await fetch("https://api.sparebank1.no/oauth/token", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - method: "POST", - body: params, - }); - const tokens = await res.json() as Sb1Tokens - const epoch = Temporal.Now.instant().epochMilliseconds - if (res.ok) { - 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 { - console.error("Failed to refresh tokens", tokens) - await add_session_log(id, "REFRESH_SB1_TOKEN", "Failed: " + JSON.stringify(res)) - return null - } - } -} - -const data = { - async get_accounts() { - const token = await auth.get_access_token() - if (!token) return undefined - const url = new URL( - "https://api.sparebank1.no/personal/banking/accounts", - ); - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${token}` - } - }) - if (response.ok) return await response.json() as { accounts: Array } - else console.error(await response.text()) - }, - 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")) - 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[] - } -} - -export default { auth, data, init } - -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" -): string { - const zdt = instant.toZonedDateTimeISO(timeZone); - - const pad = (value: number, length = 2) => - value.toString().padStart(length, "0"); - - const replacements: Record = { - yyyy: pad(zdt.year, 4), - MM: pad(zdt.month), - dd: pad(zdt.day), - HH: pad(zdt.hour), - mm: pad(zdt.minute), - ss: pad(zdt.second), - }; - - return format.replace( - /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 deleted file mode 100644 index 54498f3..0000000 --- a/app/src/lib/server/session-log.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Temporal } from "temporal-polyfill" -import { db } from "./db" -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(SyncLogTable).values({ - dateTime: String(Temporal.Now.instant().epochMilliseconds), - sessionId: id, - type: type, - msg: msg - }) -} \ No newline at end of file diff --git a/app/src/lib/shared.ts b/app/src/lib/shared.ts deleted file mode 100644 index cc4472f..0000000 --- a/app/src/lib/shared.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as v from 'valibot' - -export type ImportForm = v.InferOutput -export const ImportForm = v.object({ - budgetId: v.string(), - mappings: v.array( - v.object({ - sb1Id: v.string(), - actualId: v.string() - }) - ), - 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/lib/ui/button.svelte b/app/src/lib/ui/button.svelte deleted file mode 100644 index ad82f57..0000000 --- a/app/src/lib/ui/button.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/app/src/routes/+page.server.ts b/app/src/routes/+page.server.ts deleted file mode 100644 index df076d7..0000000 --- a/app/src/routes/+page.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { PageServerLoad } from './$types'; -import actual from '$lib/server/actual'; -import sb1 from "$lib/server/sb1" - -export const load = (async () => { - return { - actual: { - budgets: await actual.budget.get_budgets(), - accounts: await actual.budget.get_accounts(), - }, - sb1: { - accounts: (await sb1.data.get_accounts())?.accounts - } - }; -}) satisfies PageServerLoad; \ No newline at end of file diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte deleted file mode 100644 index 693a430..0000000 --- a/app/src/routes/+page.svelte +++ /dev/null @@ -1,94 +0,0 @@ - - -
- {#if data.sb1.accounts?.length} -
-

Importer

-
-

Kontoer

- {#each data.sb1.accounts as account} - {@const actualId = `mapping-${account.key}-actual`} -
- {account.name} - - - -
- {/each} -

Ellers

-

- -
-
-

Annet

- -
- {:else} - - {/if} -
diff --git a/app/src/routes/methods.remote.ts b/app/src/routes/methods.remote.ts deleted file mode 100644 index d6fd908..0000000 --- a/app/src/routes/methods.remote.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { db } from "$lib/server/db"; -import { SyncSessionTable } from "$lib/server/db/schema"; -import { command, query } from "$app/server"; -import sb1 from "$lib/server/sb1"; -import actual from "$lib/server/actual"; -import { ImportForm } from "$lib/shared"; - -const init_auth_session = command(async () => { - return await sb1.auth.init_auth_session() -}) - -const clear_auth_session = query(async () => { - await db.delete(SyncSessionTable) -}) - -const do_import = command(ImportForm, async (form) => { - for (const mapping of form.mappings) { - const transactions = await sb1.data.get_transactions(mapping.sb1Id) - if (!transactions?.length) continue - console.log(await actual.budget.import_transactions(mapping.actualId, transactions, form.dryRun)) - } -}) - -const init_sb1 = command(async () => { - return await sb1.init() -}) - -const init_actual = command(async () => { - return await actual.init() -}) - -export { - init_auth_session, - do_import, - 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 deleted file mode 100644 index d6b8fbf..0000000 --- a/app/src/routes/sb1-authorize/+server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { error, redirect, json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { db } from '$lib/server/db'; -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') - const state = url.searchParams.get('state'); - - if (!code) error(400, "?code is missing") - if (!state) error(400, "?state is missing") - - 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: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: fd - }) - - const responseJson = await response.json() - - if (response.ok) { - const epoch = Temporal.Now.instant().epochMilliseconds - 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 json(responseJson) - } -} diff --git a/app/src/routes/status.svelte b/app/src/routes/status.svelte deleted file mode 100644 index fe09193..0000000 --- a/app/src/routes/status.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
- {type} -
-
-
- - -- cgit v1.3