diff --git a/README.md b/README.md index be1861b..745ca96 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,10 @@ This application requires several environment variables to function correctly. B | `LOG_LEVEL` | The log level for the application. | `info` | ### `PLANKA_API_KEY`: -Currently, Planka does not official support API keys. This means that you will have to insert a long-lived session token in the database manually. +Planka v2 supports per-user API keys natively. Mint one with `POST /api/users/{id}/api-key` (the full key is returned only once, in `included.apiKey`) and store the result in this variable. The client sends it as the `X-Api-Key` header on every request. + +### `PLANKA_URL`: +The Planka base URL. The `/api` segment is appended automatically if missing — both `https://planka.example.com` and `https://planka.example.com/api` work. ## Code Overview diff --git a/package.json b/package.json index 0cdf9d0..5576ce1 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "prepare-husky": "husky" }, "dependencies": { - "@gewis/planka-client": "^2.0.1", - "@hey-api/client-fetch": "^0.8.4", + "@gewis/planka-client": "3.0.1", "imapflow": "^1.0.184", "log4js": "^6.9.1" }, diff --git a/src/planka.ts b/src/planka.ts index 049b008..416be35 100644 --- a/src/planka.ts +++ b/src/planka.ts @@ -1,15 +1,16 @@ import { client, createCard, - CreateCardRequest, - UpdateCardRequest, + type CreateCardData, + type GetBoardData, + type GetBoardResponse, getBoard, + type List, + type Options, updateCard, - GetBoardRequest, - GetBoardResponse, + type UpdateCardData, + withApiKey, } from '@gewis/planka-client'; -import type { List } from '@gewis/planka-client'; -import type { Client, Options } from '@hey-api/client-fetch'; import { getLogger } from 'log4js'; import type { CardEmail } from './mailer'; @@ -20,9 +21,14 @@ interface CacheEntry { preferredList: List | null; // Preference is: header if present, list called 'mail' if present, otherwise first list } +const ensureApiSuffix = (url: string): string => { + const trimmed = url.replace(/\/+$/, ''); + return trimmed.endsWith('/api') ? trimmed : `${trimmed}/api`; +}; + export default class Planka { private static instance: Planka | null = null; - private static client: Client | null = null; + private static initialized = false; private settings: { plankaUrl?: string; plankaApiKey?: string }; private static boardCache: Map = new Map(); @@ -40,18 +46,19 @@ export default class Planka { * Sets up API configuration like base URL and authentication. */ private initializeClient() { - if (!Planka.client) { - const plankaUrl = this.settings.plankaUrl || DEFAULT_PLANKA_URL; - const plankaApiKey = this.settings.plankaApiKey || process.env['PLANKA_API_KEY']; - - client.setConfig({ - baseUrl: plankaUrl, - headers: { - Authorization: `Bearer ${plankaApiKey}`, - }, - }); - Planka.client = client; + if (Planka.initialized) return; + + const plankaUrl = this.settings.plankaUrl || DEFAULT_PLANKA_URL; + const plankaApiKey = this.settings.plankaApiKey || process.env['PLANKA_API_KEY']; + if (!plankaApiKey) { + throw new Error('PLANKA_API_KEY is required (mint one via createUserApiKey).'); } + + client.setConfig({ + baseUrl: ensureApiSuffix(plankaUrl), + ...withApiKey(plankaApiKey), + }); + Planka.initialized = true; } /** @@ -71,7 +78,7 @@ export default class Planka { * Throws an error if the client isn't initialized. */ private static pre() { - if (!Planka.client) { + if (!Planka.initialized) { throw new Error('Client has not been initialized. Please call initialize() first.'); } } @@ -104,8 +111,8 @@ export default class Planka { continue; } - const board = await getBoard({ path: { id: id.toString() } } as Options); - const status = board.response.status; + const board = await getBoard({ path: { id: id.toString() } } as Options); + const status = board.response?.status ?? 0; Planka.logger.trace('caching board', id, 'status', status); if (status === 200 && board.data) { @@ -115,7 +122,7 @@ export default class Planka { if (lists.length > 0) { // Find the list named 'mail', or fall back to the first list if not found - preferredList = lists.find((list: List) => list.name.toLowerCase() === 'mail') || lists[0]; + preferredList = lists.find((list: List) => list.name?.toLowerCase() === 'mail') || lists[0]; } Planka.boardCache.set(id, { board: board.data, preferredList }); @@ -168,10 +175,10 @@ export default class Planka { name: card.title, position: 0, }, - } as Options).then(async (result) => { - Planka.logger.trace('created card', card.uid, 'status', result.response.status); + } as Options).then(async (result) => { + const status = result.response?.status ?? 0; + Planka.logger.trace('created card', card.uid, 'status', status); const cardResult = result.data; - const status = result.response.status; if (status !== 200 || !cardResult) return; @@ -187,9 +194,9 @@ export default class Planka { description: card.body, dueDate: card.date ? card.date : null, }, - } as Options) + } as Options) .then((result) => { - Planka.logger.trace('updated card', card.uid, 'status', result.response.status); + Planka.logger.trace('updated card', card.uid, 'status', result.response?.status ?? 0); }) .catch((e) => { Planka.logger.error('error updating card', card.uid, e); diff --git a/yarn.lock b/yarn.lock index 4a5fdd8..b265fda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -96,12 +96,12 @@ eslint-plugin-import "^2.31.0" typescript-eslint "^8.25.0" -"@gewis/planka-client@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@gewis/planka-client/-/planka-client-2.0.1.tgz#aa290262752f825bc3d4eaf283e2462419500f2b" - integrity sha512-1XDahfHOIXuAeMpzjlrQbwhRORbrwN8gjsuwy4W9gPrEZ1hP9BfwBlchjRC7I4Dm82pkvvHu20UWbspL3UONbA== +"@gewis/planka-client@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@gewis/planka-client/-/planka-client-3.0.1.tgz#02bb65adb5fca4cb4d825022f61c973f85e74088" + integrity sha512-2BHT4bonHWsodenhzAulW8o/CSGkO7MyfX2JtOOLmZ+VRIVNHrIFplKWwX/p6i3XBiS7sBaTtl03fiNqlMyBgQ== dependencies: - "@hey-api/client-fetch" "^0.8.1" + "@hey-api/client-fetch" "0.13.1" "@gewis/prettier-config@^2.2.2": version "2.2.2" @@ -111,10 +111,10 @@ "@vue/eslint-config-prettier" "^10.2.0" eslint-config-prettier "^10.0.1" -"@hey-api/client-fetch@^0.8.1", "@hey-api/client-fetch@^0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.8.4.tgz#2dc2d368b4dd2137789f46adcae68ccd04ea2adb" - integrity sha512-SWtUjVEFIUdiJGR2NiuF0njsSrSdTe7WHWkp3BLH3DEl2bRhiflOnBo29NSDdrY90hjtTQiTQkBxUgGOF29Xzg== +"@hey-api/client-fetch@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.13.1.tgz#d01cdeed5f5b5b5706d52ffe29387741d2a533e0" + integrity sha512-29jBRYNdxVGlx5oewFgOrkulZckpIpBIRHth3uHFn1PrL2ucMy52FvWOY3U3dVx2go1Z3kUmMi6lr07iOpUqqA== "@humanfs/core@^0.19.1": version "0.19.1"