Compare commits

...
Sign in to create a new pull request.

21 commits

Author SHA1 Message Date
Darren Clarke
21cc160f8f Redis logout WIP 2025-02-05 14:09:59 +01:00
Darren Clarke
dd0265f3f5 Update deps, add robots.txt 2025-01-26 12:24:57 +01:00
Darren Clarke
810a333429 Update logging 2025-01-22 17:50:38 +01:00
Darren Clarke
def602c05e Add ioredis, update deps 2025-01-20 11:17:37 +01:00
Darren Clarke
9e5ea2fc41 Update deps and Zammad version (6.4.1) 2025-01-15 14:15:02 +01:00
Darren Clarke
07ee819520 Revert CI to use Gitlab 2025-01-08 14:19:41 +00:00
Darren Clarke
ba0f1adcc4 Fix --password-stdin 2024-12-20 11:26:25 +01:00
Darren Clarke
5af67ef6ee Use password-stdin 2024-12-20 11:13:13 +01:00
Darren Clarke
25a5f0bc68 Disable npm/turbo install 2024-12-20 09:53:45 +01:00
Darren Clarke
ab67245b91 More CI updates 2024-12-20 09:52:01 +01:00
Darren Clarke
857d877efa CI updates 2024-12-20 09:49:29 +01:00
Darren Clarke
414502a33d Install npm 10 instead of latest 2024-12-20 09:22:23 +01:00
Darren Clarke
0525f58324 Allow api calls to signal/whatsapp 2024-12-19 22:43:17 +01:00
Darren Clarke
9fb3665ced Login, logout and middleware updates 2024-12-13 16:37:20 +01:00
Darren Clarke
f552f8024f Update deps 2024-12-13 13:20:37 +01:00
Darren Clarke
589010493d Fix addon build 2024-11-28 09:37:03 +01:00
Darren Clarke
a8dd53507d Opensearch embed changes 2024-11-28 08:27:20 +01:00
Darren Clarke
130554d86b Shorten session length, change device ID calc 2024-11-25 12:20:49 +01:00
Darren Clarke
84731c9e9a Email redirect and group dropdown fixes 2024-11-25 11:48:19 +01:00
Darren Clarke
7ad25e8a95 Update dependencies 2024-11-25 09:31:25 +01:00
Darren Clarke
48aa89f7cf Make bridge worker settings configuration via env vars 2024-11-05 10:12:18 +01:00
130 changed files with 4393 additions and 3350 deletions

View file

@ -12,7 +12,7 @@ build-all:
TURBO_TEAM: ${TURBO_TEAM} TURBO_TEAM: ${TURBO_TEAM}
ZAMMAD_URL: ${ZAMMAD_URL} ZAMMAD_URL: ${ZAMMAD_URL}
script: script:
- npm install npm@latest -g - npm install npm@10 -g
- npm install -g turbo - npm install -g turbo
- npm ci - npm ci
- turbo build - turbo build
@ -204,7 +204,7 @@ zammad-docker-build:
before_script: before_script:
- apk --update add nodejs npm - apk --update add nodejs npm
script: script:
- npm install npm@latest -g - npm install npm@10 -g
- npm install -g turbo - npm install -g turbo
- npm ci - npm ci
- turbo build --force --filter @link-stack/zammad-addon-* - turbo build --force --filter @link-stack/zammad-addon-*
@ -226,7 +226,7 @@ zammad-standalone-docker-build:
before_script: before_script:
- apk --update add nodejs npm - apk --update add nodejs npm
script: script:
- npm install npm@latest -g - npm install npm@10 -g
- npm install -g turbo - npm install -g turbo
- npm ci - npm ci
- turbo build --force --filter @link-stack/zammad-addon-* - turbo build --force --filter @link-stack/zammad-addon-*

View file

@ -1,10 +1,11 @@
import { Create } from "@link-stack/bridge-ui"; import { Create } from "@link-stack/bridge-ui";
type PageProps = { type PageProps = {
params: { segment: string[] }; params: Promise<{ segment: string[] }>;
}; };
export default function Page({ params: { segment } }: PageProps) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
return <Create service={service} />; return <Create service={service} />;

View file

@ -1,11 +1,12 @@
import { db } from "@link-stack/bridge-common"; import { db } from "@link-stack/bridge-common";
import { serviceConfig, Detail } from "@link-stack/bridge-ui"; import { serviceConfig, Detail } from "@link-stack/bridge-ui";
type Props = { type PageProps = {
params: { segment: string[] }; params: Promise<{ segment: string[] }>;
}; };
export default async function Page({ params: { segment } }: Props) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
const id = segment?.[1]; const id = segment?.[1];

View file

@ -2,10 +2,11 @@ import { db } from "@link-stack/bridge-common";
import { serviceConfig, Edit } from "@link-stack/bridge-ui"; import { serviceConfig, Edit } from "@link-stack/bridge-ui";
type PageProps = { type PageProps = {
params: { segment: string[] }; params: Promise<{ segment: string[] }>;
}; };
export default async function Page({ params: { segment } }: PageProps) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
const id = segment?.[1]; const id = segment?.[1];

View file

@ -2,12 +2,13 @@ import { db } from "@link-stack/bridge-common";
import { serviceConfig, List } from "@link-stack/bridge-ui"; import { serviceConfig, List } from "@link-stack/bridge-ui";
type PageProps = { type PageProps = {
params: { params: Promise<{
segment: string[]; segment: string[];
}; }>;
}; };
export default async function Page({ params: { segment } }: PageProps) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
if (!service) return null; if (!service) return null;

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack/bridge-frontend", "name": "@link-stack/bridge-frontend",
"version": "2.2.0", "version": "2.4.0b1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -13,26 +13,26 @@
"migrate:down:one": "tsx database/migrate.ts down:one" "migrate:down:one": "tsx database/migrate.ts down:one"
}, },
"dependencies": { "dependencies": {
"@auth/kysely-adapter": "^1.5.2", "@auth/kysely-adapter": "^1.7.4",
"@mui/icons-material": "^5", "@mui/icons-material": "^6",
"@mui/material": "^5", "@mui/material": "^6",
"@mui/material-nextjs": "^5", "@mui/material-nextjs": "^6",
"@mui/x-license": "^7.18.0", "@mui/x-license": "^7.24.1",
"@link-stack/bridge-common": "*", "@link-stack/bridge-common": "*",
"@link-stack/bridge-ui": "*", "@link-stack/bridge-ui": "*",
"next": "14.2.13", "next": "15.1.6",
"next-auth": "^4.24.8", "next-auth": "^4.24.11",
"react": "18.3.1", "react": "19.0.0",
"react-dom": "18.3.1", "react-dom": "19.0.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tsx": "^4.19.1", "tsx": "^4.19.2",
"@link-stack/ui": "*" "@link-stack/ui": "*"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22", "@types/node": "^22",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.11",
"@types/react": "^18", "@types/react": "^19",
"@types/react-dom": "^18", "@types/react-dom": "^19",
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*", "@link-stack/typescript-config": "*",
"typescript": "^5" "typescript": "^5"

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -1,6 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -14,14 +18,24 @@
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
"./*"
]
}, },
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
} }
] ],
"target": "ES2017"
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }

View file

@ -72,7 +72,7 @@ export const migrate = async (arg: string) => {
results?.forEach((it) => { results?.forEach((it) => {
if (it.status === "Success") { if (it.status === "Success") {
console.log( console.info(
`Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`, `Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`,
); );
} else if (it.status === "Error") { } else if (it.status === "Error") {

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack/bridge-migrations", "name": "@link-stack/bridge-migrations",
"version": "2.2.0", "version": "2.4.0b1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"migrate:up:all": "tsx migrate.ts up:all", "migrate:up:all": "tsx migrate.ts up:all",
@ -9,14 +9,14 @@
"migrate:down:one": "tsx migrate.ts down:one" "migrate:down:one": "tsx migrate.ts down:one"
}, },
"dependencies": { "dependencies": {
"dotenv": "^16.4.5", "dotenv": "^16.4.7",
"kysely": "0.26.1", "kysely": "0.26.1",
"pg": "^8.13.0", "pg": "^8.13.1",
"tsx": "^4.19.1" "tsx": "^4.19.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22", "@types/node": "^22",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.11",
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*", "@link-stack/typescript-config": "*",
"typescript": "^5" "typescript": "^5"

View file

@ -1,26 +1,26 @@
{ {
"name": "@link-stack/bridge-whatsapp", "name": "@link-stack/bridge-whatsapp",
"version": "2.2.0", "version": "2.4.0b1",
"main": "build/main/index.js", "main": "build/main/index.js",
"author": "Darren Clarke <darren@redaranj.com>", "author": "Darren Clarke <darren@redaranj.com>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@adiwajshing/keyed-db": "0.2.4", "@adiwajshing/keyed-db": "0.2.4",
"@hapi/hapi": "^21.3.10", "@hapi/hapi": "^21.3.12",
"@hapipal/schmervice": "^3.0.0", "@hapipal/schmervice": "^3.0.0",
"@hapipal/toys": "^4.0.0", "@hapipal/toys": "^4.0.0",
"@whiskeysockets/baileys": "^6.7.8", "@whiskeysockets/baileys": "^6.7.9",
"hapi-pino": "^12.1.0", "hapi-pino": "^12.1.0",
"link-preview-js": "^3.0.5" "link-preview-js": "^3.0.13"
}, },
"devDependencies": { "devDependencies": {
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@link-stack/jest-config": "*", "@link-stack/jest-config": "*",
"@link-stack/typescript-config": "*", "@link-stack/typescript-config": "*",
"@types/node": "*", "@types/node": "*",
"dotenv-cli": "^7.4.2", "dotenv-cli": "^8.0.0",
"tsx": "^4.19.1", "tsx": "^4.19.2",
"typescript": "^5.6.2" "typescript": "^5.7.3"
}, },
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",

View file

@ -26,7 +26,6 @@ export const SendMessageRoute = withDefaults({
description: "Send a message", description: "Send a message",
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
const { id } = request.params; const { id } = request.params;
console.log({ payload: request.payload });
const { phoneNumber, message } = request.payload as MessageRequest; const { phoneNumber, message } = request.payload as MessageRequest;
const whatsappService = getService(request); const whatsappService = getService(request);
await whatsappService.send(id, phoneNumber, message as string); await whatsappService.send(id, phoneNumber, message as string);

View file

@ -57,7 +57,7 @@ export default class WhatsappService extends Service {
try { try {
connection.end(null); connection.end(null);
} catch (error) { } catch (error) {
console.log(error); console.error(error);
} }
} }
this.connections = {}; this.connections = {};
@ -92,27 +92,27 @@ export default class WhatsappService extends Service {
isNewLogin, isNewLogin,
} = update; } = update;
if (qr) { if (qr) {
console.log("got qr code"); console.info("got qr code");
const botDirectory = this.getBotDirectory(botID); const botDirectory = this.getBotDirectory(botID);
const qrPath = `${botDirectory}/qr.txt`; const qrPath = `${botDirectory}/qr.txt`;
fs.writeFileSync(qrPath, qr, "utf8"); fs.writeFileSync(qrPath, qr, "utf8");
} else if (isNewLogin) { } else if (isNewLogin) {
console.log("got new login"); console.info("got new login");
const botDirectory = this.getBotDirectory(botID); const botDirectory = this.getBotDirectory(botID);
const verifiedFile = `${botDirectory}/verified`; const verifiedFile = `${botDirectory}/verified`;
fs.writeFileSync(verifiedFile, ""); fs.writeFileSync(verifiedFile, "");
} else if (connectionState === "open") { } else if (connectionState === "open") {
console.log("opened connection"); console.info("opened connection");
} else if (connectionState === "close") { } else if (connectionState === "close") {
console.log("connection closed due to ", lastDisconnect?.error); console.info("connection closed due to ", lastDisconnect?.error);
const disconnectStatusCode = (lastDisconnect?.error as any)?.output const disconnectStatusCode = (lastDisconnect?.error as any)?.output
?.statusCode; ?.statusCode;
if (disconnectStatusCode === DisconnectReason.restartRequired) { if (disconnectStatusCode === DisconnectReason.restartRequired) {
console.log("reconnecting after got new login"); console.info("reconnecting after got new login");
await this.createConnection(botID, server, options); await this.createConnection(botID, server, options);
authCompleteCallback?.(); authCompleteCallback?.();
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) { } else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
console.log("reconnecting"); console.info("reconnecting");
await this.sleep(pause); await this.sleep(pause);
pause *= 2; pause *= 2;
this.createConnection(botID, server, options); this.createConnection(botID, server, options);
@ -121,12 +121,12 @@ export default class WhatsappService extends Service {
} }
if (events["creds.update"]) { if (events["creds.update"]) {
console.log("creds update"); console.info("creds update");
await saveCreds(); await saveCreds();
} }
if (events["messages.upsert"]) { if (events["messages.upsert"]) {
console.log("messages upsert"); console.info("messages upsert");
const upsert = events["messages.upsert"]; const upsert = events["messages.upsert"];
const { messages } = upsert; const { messages } = upsert;
if (messages) { if (messages) {
@ -143,13 +143,13 @@ export default class WhatsappService extends Service {
const baseDirectory = this.getBaseDirectory(); const baseDirectory = this.getBaseDirectory();
const botIDs = fs.readdirSync(baseDirectory); const botIDs = fs.readdirSync(baseDirectory);
console.log({ botIDs });
for await (const botID of botIDs) { for await (const botID of botIDs) {
const directory = this.getBotDirectory(botID); const directory = this.getBotDirectory(botID);
const verifiedFile = `${directory}/verified`; const verifiedFile = `${directory}/verified`;
if (fs.existsSync(verifiedFile)) { if (fs.existsSync(verifiedFile)) {
const { version, isLatest } = await fetchLatestBaileysVersion(); const { version, isLatest } = await fetchLatestBaileysVersion();
console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`); console.info(`using WA v${version.join(".")}, isLatest: ${isLatest}`);
await this.createConnection(botID, this.server, { await this.createConnection(botID, this.server, {
browser: WhatsappService.browserDescription, browser: WhatsappService.browserDescription,
@ -169,7 +169,10 @@ export default class WhatsappService extends Service {
message, message,
messageTimestamp, messageTimestamp,
} = webMessageInfo; } = webMessageInfo;
console.log(webMessageInfo); console.info("Message type debug");
for (const key in message) {
console.info(key, !!message[key as keyof proto.IMessage]);
}
const isValidMessage = const isValidMessage =
message && remoteJid !== "status@broadcast" && !fromMe; message && remoteJid !== "status@broadcast" && !fromMe;
if (isValidMessage) { if (isValidMessage) {

View file

@ -4,8 +4,12 @@ import type {} from "graphile-worker";
const preset: GraphileConfig.Preset = { const preset: GraphileConfig.Preset = {
worker: { worker: {
connectionString: process.env.DATABASE_URL, connectionString: process.env.DATABASE_URL,
maxPoolSize: 10, maxPoolSize: process.env.BRIDGE_WORKER_POOL_SIZE
pollInterval: 2000, ? parseInt(process.env.BRIDGE_WORKER_POOL_SIZE, 10)
: 10,
pollInterval: process.env.BRIDGE_WORKER_POLL_INTERVAL
? parseInt(process.env.BRIDGE_WORKER_POLL_INTERVAL, 10)
: 2000,
fileExtensions: [".ts"], fileExtensions: [".ts"],
}, },
}; };

View file

@ -6,13 +6,20 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const startWorker = async () => { const startWorker = async () => {
console.log("Starting worker..."); console.info("Starting worker...");
console.log(process.env);
await run({ await run({
connectionString: process.env.DATABASE_URL, connectionString: process.env.DATABASE_URL,
concurrency: 10,
noHandleSignals: false, noHandleSignals: false,
pollInterval: 1000, concurrency: process.env.BRIDGE_WORKER_CONCURRENCY
? parseInt(process.env.BRIDGE_WORKER_CONCURRENCY, 10)
: 10,
maxPoolSize: process.env.BRIDGE_WORKER_POOL_SIZE
? parseInt(process.env.BRIDGE_WORKER_POOL_SIZE, 10)
: 10,
pollInterval: process.env.BRIDGE_WORKER_POLL_INTERVAL
? parseInt(process.env.BRIDGE_WORKER_POLL_INTERVAL, 10)
: 1000,
taskDirectory: `${__dirname}/tasks`, taskDirectory: `${__dirname}/tasks`,
crontabFile: `${__dirname}/crontab`, crontabFile: `${__dirname}/crontab`,
}); });

View file

@ -62,9 +62,8 @@ export const createZammadTicket = async (
}, },
}); });
} catch (error: any) { } catch (error: any) {
console.log(Object.keys(error));
if (error.isBoom) { if (error.isBoom) {
console.log(error.output); console.error(error.output);
throw new Error("Failed to create zamamd ticket"); throw new Error("Failed to create zamamd ticket");
} }
} }

View file

@ -25,7 +25,7 @@ const defaultAudioConvertOpts = {
**/ **/
export const convert = ( export const convert = (
input: Buffer, input: Buffer,
opts?: AudioConvertOpts opts?: AudioConvertOpts,
): Promise<Buffer> => { ): Promise<Buffer> => {
const settings = { ...defaultAudioConvertOpts, ...opts }; const settings = { ...defaultAudioConvertOpts, ...opts };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -35,12 +35,8 @@ export const convert = (
.audioCodec(settings.audioCodec) .audioCodec(settings.audioCodec)
.audioBitrate(settings.bitrate) .audioBitrate(settings.bitrate)
.toFormat(settings.format) .toFormat(settings.format)
.on("error", (err, stdout, stderr) => { .on("error", (err, _stdout, _stderr) => {
console.error(err.message); console.error(err.message);
console.log("FFMPEG OUTPUT");
console.log(stdout);
console.log("FFMPEG ERROR");
console.log(stderr);
reject(err); reject(err);
}) })
.on("end", () => { .on("end", () => {
@ -66,8 +62,12 @@ export const selfCheck = (): Promise<boolean> => {
resolve(false); resolve(false);
} }
const preds = R.map(requiredCodecs, (codec) => (available: any) => const preds = R.map(
available[codec] && available[codec].canDemux && available[codec].canMux requiredCodecs,
(codec) => (available: any) =>
available[codec] &&
available[codec].canDemux &&
available[codec].canMux,
); );
resolve(R.allPass(codecs, preds)); resolve(R.allPass(codecs, preds));
@ -79,6 +79,6 @@ export const assertFfmpegAvailable = async (): Promise<void> => {
const r = await selfCheck(); const r = await selfCheck();
if (!r) if (!r)
throw new Error( throw new Error(
`ffmpeg is not installed, could not be located, or does not support the required codecs: ${requiredCodecs}` `ffmpeg is not installed, could not be located, or does not support the required codecs: ${requiredCodecs}`,
); );
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack/bridge-worker", "name": "@link-stack/bridge-worker",
"version": "2.2.0", "version": "2.4.0b1",
"type": "module", "type": "module",
"main": "build/main/index.js", "main": "build/main/index.js",
"author": "Darren Clarke <darren@redaranj.com>", "author": "Darren Clarke <darren@redaranj.com>",
@ -16,14 +16,14 @@
"@link-stack/signal-api": "*", "@link-stack/signal-api": "*",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"graphile-worker": "^0.16.6", "graphile-worker": "^0.16.6",
"remeda": "^2.14.0", "remeda": "^2.20.0",
"twilio": "^5.3.2" "twilio": "^5.4.2"
}, },
"devDependencies": { "devDependencies": {
"@types/fluent-ffmpeg": "^2.1.26", "@types/fluent-ffmpeg": "^2.1.27",
"dotenv-cli": "^7.4.2", "dotenv-cli": "^8.0.0",
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*", "@link-stack/typescript-config": "*",
"typescript": "^5.6.2" "typescript": "^5.7.3"
} }
} }

View file

@ -19,13 +19,11 @@ const notifyWebhooksTask = async (
for (const webhook of webhooks) { for (const webhook of webhooks) {
const { endpointUrl, httpMethod, headers } = webhook; const { endpointUrl, httpMethod, headers } = webhook;
const finalHeaders = { "Content-Type": "application/json", ...headers }; const finalHeaders = { "Content-Type": "application/json", ...headers };
console.log({ endpointUrl, httpMethod, headers, finalHeaders });
const result = await fetch(endpointUrl, { const result = await fetch(endpointUrl, {
method: httpMethod, method: httpMethod,
headers: finalHeaders, headers: finalHeaders,
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
console.log(result);
} }
}; };

View file

@ -31,7 +31,6 @@ const sendFacebookMessageTask = async (
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(outgoingMessage), body: JSON.stringify(outgoingMessage),
}); });
console.log({ response });
} catch (error) { } catch (error) {
console.error({ error }); console.error({ error });
throw error; throw error;

View file

@ -46,7 +46,6 @@ const processMessage = async ({
message: msg, message: msg,
}: ProcessMessageArgs): Promise<Record<string, any>[]> => { }: ProcessMessageArgs): Promise<Record<string, any>[]> => {
const { envelope } = msg; const { envelope } = msg;
console.log(envelope);
const { source, sourceUuid, dataMessage } = envelope; const { source, sourceUuid, dataMessage } = envelope;
if (!dataMessage) return []; if (!dataMessage) return [];
@ -125,7 +124,6 @@ const fetchSignalMessagesTask = async ({
phoneNumber, phoneNumber,
message, message,
}); });
console.log({ formattedMessages });
for (const formattedMessage of formattedMessages) { for (const formattedMessage of formattedMessages) {
if (formattedMessage.to !== formattedMessage.from) { if (formattedMessage.to !== formattedMessage.from) {
await worker.addJob( await worker.addJob(

View file

@ -36,7 +36,6 @@ const getZammadTickets = async (
{ headers }, { headers },
); );
const tickets: any = await rawTickets.json(); const tickets: any = await rawTickets.json();
console.log({ tickets });
if (!tickets || tickets.length === 0) { if (!tickets || tickets.length === 0) {
return [shouldContinue, docs]; return [shouldContinue, docs];
} }
@ -49,22 +48,8 @@ const getZammadTickets = async (
shouldContinue = true; shouldContinue = true;
if (source_closed_at <= minUpdatedTimestamp) { if (source_closed_at <= minUpdatedTimestamp) {
console.log(`Skipping ticket`, {
source_id,
source_updated_at,
source_closed_at,
minUpdatedTimestamp,
});
continue; continue;
} }
console.log(`Processing ticket`, {
source_id,
source_updated_at,
source_closed_at,
minUpdatedTimestamp,
});
const rawArticles = await fetch( const rawArticles = await fetch(
`${zammadApiUrl}/ticket_articles/by_ticket/${source_id}`, `${zammadApiUrl}/ticket_articles/by_ticket/${source_id}`,
{ headers }, { headers },
@ -178,8 +163,6 @@ const sendToLabelStudio = async (tickets: FormattedZammadTicket[]) => {
body: JSON.stringify([ticket]), body: JSON.stringify([ticket]),
}); });
const importResult = await res.json(); const importResult = await res.json();
console.log(JSON.stringify(importResult, undefined, 2));
} }
}; };
*/ */
@ -201,7 +184,6 @@ const importLabelStudioTask = async (): Promise<void> => {
await sendToLabelStudio(tickets); await sendToLabelStudio(tickets);
const lastTicket = tickets.pop(); const lastTicket = tickets.pop();
const newLastTimestamp = lastTicket.data.source_closed_at; const newLastTimestamp = lastTicket.data.source_closed_at;
console.log({ newLastTimestamp });
await db.settings.upsert(settingName, { await db.settings.upsert(settingName, {
minUpdatedTimestamp: newLastTimestamp, minUpdatedTimestamp: newLastTimestamp,
}); });

View file

@ -43,14 +43,11 @@ const getLabelStudioTickets = async (
page_size: "50", page_size: "50",
page: `${page}`, page: `${page}`,
}); });
console.log({ url: `${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}` });
const res = await fetch( const res = await fetch(
`${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}`, `${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}`,
{ headers }, { headers },
); );
console.log({ res });
const tasksResult: any = await res.json(); const tasksResult: any = await res.json();
console.log({ tasksResult });
return tasksResult; return tasksResult;
}; };
@ -63,14 +60,11 @@ const fetchFromLabelStudio = async (
for await (const page of pages) { for await (const page of pages) {
const docs = await getLabelStudioTickets(page + 1); const docs = await getLabelStudioTickets(page + 1);
console.log({ page, docs });
if (docs && docs.length > 0) { if (docs && docs.length > 0) {
for (const doc of docs) { for (const doc of docs) {
const updatedAt = new Date(doc.updated_at); const updatedAt = new Date(doc.updated_at);
console.log({ updatedAt, minUpdatedTimestamp });
if (updatedAt > minUpdatedTimestamp) { if (updatedAt > minUpdatedTimestamp) {
console.log(`Adding doc`, { doc });
allDocs.push(doc); allDocs.push(doc);
} }
} }
@ -79,7 +73,6 @@ const fetchFromLabelStudio = async (
} }
} }
console.log({ allDocs });
return allDocs; return allDocs;
}; };
@ -93,9 +86,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
}, },
} = config; } = config;
console.log({ tickets });
const filteredTickets = tickets.filter((ticket) => ticket.is_labeled); const filteredTickets = tickets.filter((ticket) => ticket.is_labeled);
console.log({ filteredTickets });
const finalTickets: LeafcutterTicket[] = filteredTickets.map((ticket) => { const finalTickets: LeafcutterTicket[] = filteredTickets.map((ticket) => {
const { const {
id, id,
@ -131,8 +122,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
}; };
}); });
console.log("Sending to Leafcutter"); console.info("Sending to Leafcutter");
console.log({ finalTickets });
const result = await fetch(opensearchApiUrl, { const result = await fetch(opensearchApiUrl, {
method: "POST", method: "POST",
@ -157,15 +147,7 @@ const importLeafcutterTask = async (): Promise<void> => {
? new Date(res.value.minUpdatedTimestamp as string) ? new Date(res.value.minUpdatedTimestamp as string)
: new Date("2023-03-01"); : new Date("2023-03-01");
const newLastTimestamp = new Date(); const newLastTimestamp = new Date();
console.log({
contributorName,
settingName,
res,
startTimestamp,
newLastTimestamp,
});
const tickets = await fetchFromLabelStudio(startTimestamp); const tickets = await fetchFromLabelStudio(startTimestamp);
console.log({ tickets });
await sendToLeafcutter(tickets); await sendToLeafcutter(tickets);
await db.settings.upsert(settingName, { await db.settings.upsert(settingName, {
minUpdatedTimestamp: newLastTimestamp, minUpdatedTimestamp: newLastTimestamp,

View file

@ -23,7 +23,6 @@ const receiveSignalMessageTask = async ({
filename, filename,
mimeType, mimeType,
}: ReceiveSignalMessageTaskOptions): Promise<void> => { }: ReceiveSignalMessageTaskOptions): Promise<void> => {
console.log({ token, to, from });
const worker = await getWorkerUtils(); const worker = await getWorkerUtils();
const row = await db const row = await db
.selectFrom("SignalBot") .selectFrom("SignalBot")

View file

@ -13,7 +13,6 @@ const sendSignalMessageTask = async ({
to, to,
message, message,
}: SendSignalMessageTaskOptions): Promise<void> => { }: SendSignalMessageTaskOptions): Promise<void> => {
console.log({ token, to });
const bot = await db const bot = await db
.selectFrom("SignalBot") .selectFrom("SignalBot")
.selectAll() .selectAll()
@ -34,7 +33,6 @@ const sendSignalMessageTask = async ({
message, message,
}, },
}); });
console.log({ response });
} catch (error) { } catch (error) {
console.error({ error }); console.error({ error });
throw error; throw error;

View file

@ -23,8 +23,6 @@ const receiveWhatsappMessageTask = async ({
filename, filename,
mimeType, mimeType,
}: ReceiveWhatsappMessageTaskOptions): Promise<void> => { }: ReceiveWhatsappMessageTaskOptions): Promise<void> => {
console.log({ token, to, from });
const worker = await getWorkerUtils(); const worker = await getWorkerUtils();
const row = await db const row = await db
.selectFrom("WhatsappBot") .selectFrom("WhatsappBot")

View file

@ -25,7 +25,6 @@ const sendWhatsappMessageTask = async ({
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(params), body: JSON.stringify(params),
}); });
console.log({ result });
} catch (error) { } catch (error) {
console.error({ error }); console.error({ error });
throw new Error("Failed to send message"); throw new Error("Failed to send message");

View file

@ -1,5 +1,10 @@
import { Suspense } from "react";
import { About } from "@link-stack/leafcutter-ui"; import { About } from "@link-stack/leafcutter-ui";
export default function Page() { export default function Page() {
return <About />; return (
<Suspense>
<About />
</Suspense>
);
} }

View file

@ -1,5 +1,12 @@
import { Suspense } from "react";
import { FAQ } from "@link-stack/leafcutter-ui"; import { FAQ } from "@link-stack/leafcutter-ui";
export default function Page() { export const dynamic = "force-dynamic";
return <FAQ />;
export default async function Page() {
return (
<Suspense>
<FAQ />
</Suspense>
);
} }

View file

@ -2,6 +2,8 @@ import { ReactNode } from "react";
import "app/_styles/global.css"; import "app/_styles/global.css";
import { InternalLayout } from "../_components/InternalLayout"; import { InternalLayout } from "../_components/InternalLayout";
export const dynamic = "force-dynamic";
type LayoutProps = { type LayoutProps = {
children: ReactNode; children: ReactNode;
}; };

View file

@ -69,19 +69,17 @@ export const getServerSideProps: GetServerSideProps = async (
}); });
}); });
} }
console.log({ query });
const dataResponse = await client.search({ const dataResponse = await client.search({
index: "demo_data", index: "demo_data",
size: 200, size: 200,
body: { query }, body: { query },
}); });
console.log({ dataResponse });
res.props.data = dataResponse.body.hits.hits.map((hit) => ({ res.props.data = dataResponse.body.hits.hits.map((hit) => ({
id: hit._id, id: hit._id,
...hit._source, ...hit._source,
})); }));
console.log({ data: res.props.data });
console.log(res.props.data[0]);
return res; return res;
}; };

View file

@ -13,7 +13,7 @@ export const Setup: FC = () => {
} = useLeafcutterContext(); } = useLeafcutterContext();
const router = useRouter(); const router = useRouter();
useLayoutEffect(() => { useLayoutEffect(() => {
setTimeout(() => router.push("/"), 4000); setTimeout(() => router.push("/"), 2000);
}, [router]); }, [router]);
return ( return (

View file

@ -1,5 +1,10 @@
import { Suspense } from "react";
import { Setup } from "./_components/Setup"; import { Setup } from "./_components/Setup";
export default function Page() { export default function Page() {
return <Setup />; return (
<Suspense>
<Setup />
</Suspense>
);
} }

View file

@ -22,9 +22,9 @@ const getVisualization = async (visualizationID: string) => {
); );
const hit = hits[0]; const hit = hits[0];
const visualization = { const visualization = {
id: hit._id.split(":")[1], id: hit?._id.split(":")[1],
title: hit._source.visualization.title, title: hit?._source?.visualization.title,
description: hit._source.visualization.description, description: hit?._source?.visualization.description,
url: `/app/visualize?security_tenant=global#/edit/${ url: `/app/visualize?security_tenant=global#/edit/${
hit._id.split(":")[1] hit._id.split(":")[1]
}?embed=true`, }?embed=true`,
@ -34,12 +34,13 @@ const getVisualization = async (visualizationID: string) => {
}; };
type PageProps = { type PageProps = {
params: { params: Promise<{
visualizationID: string; visualizationID: string;
}; }>;
}; };
export default async function Page({ params: { visualizationID } }: PageProps) { export default async function Page({ params }: PageProps) {
const { visualizationID } = await params;
const visualization = await getVisualization(visualizationID); const visualization = await getVisualization(visualizationID);
return <VisualizationDetail {...visualization} editing={false} />; return <VisualizationDetail {...visualization} editing={false} />;

View file

@ -48,7 +48,6 @@ const MenuItem = ({
return ( return (
<Link href={href} passHref> <Link href={href} passHref>
<ListItem <ListItem
button
sx={{ sx={{
paddingLeft: "62px", paddingLeft: "62px",
backgroundColor: selected ? leafcutterLightBlue : "transparent", backgroundColor: selected ? leafcutterLightBlue : "transparent",

View file

@ -22,20 +22,17 @@ export const authOptions: NextAuthOptions = {
Credentials({ Credentials({
name: "Link", name: "Link",
credentials: { credentials: {
authToken: { label: "AuthToken", type: "text", }, authToken: { label: "AuthToken", type: "text" },
}, },
async authorize(credentials, req) { async authorize(credentials, req) {
const { headers } = req; const { headers } = req;
console.log({ headers });
const leafcutterUser = headers?.["x-leafcutter-user"]; const leafcutterUser = headers?.["x-leafcutter-user"];
const authToken = credentials?.authToken; const authToken = credentials?.authToken;
if (!leafcutterUser || leafcutterUser.trim() === "") { if (!leafcutterUser || leafcutterUser.trim() === "") {
console.log("no leafcutter user");
return null; return null;
} }
console.log({ authToken });
return null; return null;
/* /*
try { try {
@ -48,14 +45,13 @@ export const authOptions: NextAuthOptions = {
return user; return user;
} catch (e) { } catch (e) {
console.log({ e }); console.error({ e });
} }
return null; return null;
*/ */
} },
}) }),
], ],
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET,
/* /*
@ -77,4 +73,3 @@ export const authOptions: NextAuthOptions = {
} }
},*/ },*/
}; };

View file

@ -10,7 +10,8 @@ const userMetadataIndexName = "user_metadata";
const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`; const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
const createClient = () => new Client({ const createClient = () =>
new Client({
node: baseURL, node: baseURL,
auth: { auth: {
username: process.env.OPENSEARCH_USERNAME!, username: process.env.OPENSEARCH_USERNAME!,
@ -21,7 +22,8 @@ const createClient = () => new Client({
}, },
}); });
const createUserClient = (username: string, password: string) => new Client({ const createUserClient = (username: string, password: string) =>
new Client({
node: baseURL, node: baseURL,
auth: { auth: {
username, username,
@ -115,7 +117,7 @@ export const getUserMetadata = async (username: string) => {
await client.create({ await client.create({
id: username, id: username,
index: userMetadataIndexName, index: userMetadataIndexName,
body: { username, savedSearches: [] } body: { username, savedSearches: [] },
}); });
res = await client.get({ res = await client.get({
@ -132,7 +134,7 @@ export const saveUserMetadata = async (username: string, metadata: any) => {
await client.update({ await client.update({
id: username, id: username,
index: userMetadataIndexName, index: userMetadataIndexName,
body: { doc: { username, ...metadata } } body: { doc: { username, ...metadata } },
}); });
}; };
@ -181,7 +183,7 @@ const getIndexPattern: any = async (index: string) => {
sort: ["updated_at:desc"], sort: ["updated_at:desc"],
}); });
if (res.body.hits.total.value === 0) { if (res?.body?.hits?.total?.valueOf() === 0) {
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
return createCurrentUserIndexPattern(index); return createCurrentUserIndexPattern(index);
} }
@ -226,7 +228,7 @@ interface createUserVisualizationProps {
} }
export const createUserVisualization = async ( export const createUserVisualization = async (
props: createUserVisualizationProps props: createUserVisualizationProps,
) => { ) => {
const { email, query, visualizationID, title, description } = props; const { email, query, visualizationID, title, description } = props;
const userIndex = await getCurrentUserIndex(email); const userIndex = await getCurrentUserIndex(email);
@ -279,7 +281,7 @@ interface updateVisualizationProps {
} }
export const updateUserVisualization = async ( export const updateUserVisualization = async (
props: updateVisualizationProps props: updateVisualizationProps,
) => { ) => {
const { email, id, query, title, description } = props; const { email, id, query, title, description } = props;
const userIndex = await getCurrentUserIndex(email); const userIndex = await getCurrentUserIndex(email);
@ -300,8 +302,7 @@ export const updateUserVisualization = async (
body, body,
}); });
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console console.error({ e });
console.log({ e });
} }
return id; return id;
@ -469,10 +470,18 @@ export const performQuery = async (searchQuery: any, limit: number) => {
const results = hits.map((hit: any) => ({ const results = hits.map((hit: any) => ({
...hit._source, ...hit._source,
id: hit._id, id: hit._id,
incident: Array.isArray(hit._source.incident) ? hit._source.incident.join(", ") : hit._source.incident, incident: Array.isArray(hit._source.incident)
technology: Array.isArray(hit._source.technology) ? hit._source.technology.join(", ") : hit._source.technology, ? hit._source.incident.join(", ")
targeted_group: Array.isArray(hit._source.targeted_group) ? hit._source.targeted_group.join(", ") : hit._source.targeted_group, : hit._source.incident,
country: Array.isArray(hit._source.country) ? hit._source.country.join(", ") : hit._source.country, technology: Array.isArray(hit._source.technology)
? hit._source.technology.join(", ")
: hit._source.technology,
targeted_group: Array.isArray(hit._source.targeted_group)
? hit._source.targeted_group.join(", ")
: hit._source.targeted_group,
country: Array.isArray(hit._source.country)
? hit._source.country.join(", ")
: hit._source.country,
})); }));
return results; return results;
@ -570,7 +579,6 @@ export const getTemplates = async (limit: number) => {
}, },
}; };
const rawResponse = await client.search({ const rawResponse = await client.search({
index: globalIndex, index: globalIndex,
size: limit, size: limit,

View file

@ -2,14 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
export const GET = async (req: NextRequest) => { export const GET = async (req: NextRequest) => {
const validDomains = "localhost"; const validDomains = "localhost";
console.log({ req });
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };
export const POST = async (req: NextRequest) => { export const POST = async (req: NextRequest) => {
const validDomains = "localhost"; const validDomains = "localhost";
console.log({ req });
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };

View file

@ -3,9 +3,8 @@ import { getTrends } from "app/_lib/opensearch";
export const GET = async () => { export const GET = async () => {
const results = await getTrends(5); const results = await getTrends(5);
console.log({ results });
NextResponse.json(results); NextResponse.json(results);
}; };
export const dynamic = 'force-dynamic'; export const dynamic = "force-dynamic";

View file

@ -15,8 +15,8 @@ export const POST = async (req: NextRequest) => {
rejectUnauthorized: false, rejectUnauthorized: false,
}, },
headers: { headers: {
authorization authorization,
} },
}); });
const succeeded = []; const succeeded = [];
@ -28,11 +28,15 @@ export const POST = async (req: NextRequest) => {
const country = ticket.country[0] ?? "none"; const country = ticket.country[0] ?? "none";
// @ts-expect-error // @ts-expect-error
const translatedCountry = taxonomy.country[country]?.display ?? "none"; const translatedCountry = taxonomy.country[country]?.display ?? "none";
const countryDetails: any = unRegions.find((c) => c.name === translatedCountry); const countryDetails: any = unRegions.find(
(c) => c.name === translatedCountry,
);
const augmentedTicket = { const augmentedTicket = {
...ticket, ...ticket,
region: countryDetails['sub-region']?.toLowerCase().replace(" ", "-") ?? null, region:
continent: countryDetails.region?.toLowerCase().replace(" ", "-") ?? null, countryDetails["sub-region"]?.toLowerCase().replace(" ", "-") ?? null,
continent:
countryDetails.region?.toLowerCase().replace(" ", "-") ?? null,
}; };
const out = await client.create({ const out = await client.create({
id: uuid(), id: uuid(),
@ -40,10 +44,9 @@ export const POST = async (req: NextRequest) => {
refresh: true, refresh: true,
body: augmentedTicket, body: augmentedTicket,
}); });
console.log(out);
succeeded.push(id); succeeded.push(id);
} catch (e) { } catch (e) {
console.log(e); console.error(e);
failed.push(id); failed.push(id);
} }
} }
@ -52,4 +55,3 @@ export const POST = async (req: NextRequest) => {
return NextResponse.json(results); return NextResponse.json(results);
}; };

View file

@ -3,13 +3,13 @@ import { getServerSession } from "next-auth";
import { authOptions } from "app/_lib/auth"; import { authOptions } from "app/_lib/auth";
import { deleteUserVisualization } from "app/_lib/opensearch"; import { deleteUserVisualization } from "app/_lib/opensearch";
export const POST = async (req: NextRequest, res: NextResponse) => { export const POST = async (req: NextRequest) => {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const { user: { email } }: any = session; const {
user: { email },
}: any = session;
const { id } = await req.json(); const { id } = await req.json();
await deleteUserVisualization(email as string, id); await deleteUserVisualization(email as string, id);
return NextResponse.json({ id }); return NextResponse.json({ id });
}; };

View file

@ -3,4 +3,4 @@
/// <reference types="next/navigation-types/compat/navigation" /> /// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -1,8 +1,5 @@
module.exports = { module.exports = {
transpilePackages: ["@link-stack/leafcutter-ui", "@link-stack/opensearch-common"], transpilePackages: ["@link-stack/leafcutter-ui", "@link-stack/opensearch-common"],
experimental: {
missingSuspenseWithCSRBailout: false,
},
poweredByHeader: false, poweredByHeader: false,
rewrites: async () => ({ rewrites: async () => ({
fallback: [ fallback: [

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack/leafcutter", "name": "@link-stack/leafcutter",
"version": "2.2.0", "version": "2.4.0b1",
"scripts": { "scripts": {
"dev": "next dev -p 3001", "dev": "next dev -p 3001",
"login": "aws sso login --sso-session cdr", "login": "aws sso login --sso-session cdr",
@ -13,37 +13,37 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@emotion/cache": "^11.13.1", "@emotion/cache": "^11.14.0",
"@emotion/react": "^11.13.3", "@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.14.0",
"@link-stack/leafcutter-ui": "*", "@link-stack/leafcutter-ui": "*",
"@link-stack/opensearch-common": "*", "@link-stack/opensearch-common": "*",
"@mui/icons-material": "^5", "@mui/icons-material": "^6",
"@mui/material": "^5", "@mui/material": "^6",
"@mui/material-nextjs": "^5", "@mui/material-nextjs": "^6",
"@mui/x-date-pickers-pro": "^7.18.0", "@mui/x-date-pickers-pro": "^7.24.1",
"@opensearch-project/opensearch": "^2.12.0", "@opensearch-project/opensearch": "^3.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"http-proxy-middleware": "^3.0.2", "http-proxy-middleware": "^3.0.3",
"material-ui-popup-state": "^5.3.1", "material-ui-popup-state": "^5.3.3",
"next": "14.2.13", "next": "15.1.6",
"next-auth": "^4.24.8", "next-auth": "^4.24.11",
"react": "18.3.1", "react": "19.0.0",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.2",
"react-cookie-consent": "^9.0.0", "react-cookie-consent": "^9.0.0",
"react-dom": "18.3.1", "react-dom": "19.0.0",
"react-iframe": "^1.8.5", "react-iframe": "^1.8.5",
"react-polyglot": "^0.7.2", "react-polyglot": "^0.7.2",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"uuid": "^10.0.0" "uuid": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.7.3", "@types/node": "^22.10.10",
"@types/react": "18.3.9", "@types/react": "19.0.8",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*", "@link-stack/typescript-config": "*",
"typescript": "5.6.2" "typescript": "5.7.3"
} }
} }

View file

@ -24,17 +24,17 @@ const withAuthInfo =
const requestSignature = req.query.signature; const requestSignature = req.query.signature;
const url = new URL(req.headers.referer as string); const url = new URL(req.headers.referer as string);
const referrerSignature = url.searchParams.get("signature"); const referrerSignature = url.searchParams.get("signature");
console.log({ requestSignature, referrerSignature });
const isAppPath = !!req.url?.startsWith("/app"); const isAppPath = !!req.url?.startsWith("/app");
const isResourcePath = !!req.url?.match(/\/(api|app|bootstrap|3961|ui|translations|internal|login|node_modules)/); const isResourcePath = !!req.url?.match(
/\/(api|app|bootstrap|3961|ui|translations|internal|login|node_modules)/,
);
if (requestSignature && isAppPath) { if (requestSignature && isAppPath) {
console.log("Has Signature"); console.info("Has Signature");
} }
if (referrerSignature && isResourcePath) { if (referrerSignature && isResourcePath) {
console.log("Has Signature"); console.info("Has Signature");
} }
if (!email) { if (!email) {

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -29,6 +29,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
typeof window !== "undefined" && window.location.origin typeof window !== "undefined" && window.location.origin
? window.location.origin ? window.location.origin
: ""; : "";
const callbackUrl = `${origin}/setup`;
const [provider, setProvider] = useState(undefined); const [provider, setProvider] = useState(undefined);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -157,7 +158,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
sx={buttonStyles} sx={buttonStyles}
onClick={() => onClick={() =>
signIn("google", { signIn("google", {
callbackUrl: `${origin}`, callbackUrl,
}) })
} }
> >
@ -173,7 +174,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
sx={buttonStyles} sx={buttonStyles}
onClick={() => onClick={() =>
signIn("apple", { signIn("apple", {
callbackUrl: `${window.location.origin}`, callbackUrl,
}) })
} }
> >
@ -214,7 +215,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
signIn("credentials", { signIn("credentials", {
email, email,
password, password,
callbackUrl: `${origin}/setup`, callbackUrl,
}) })
} }
> >

View file

@ -1,8 +1,9 @@
"use client"; "use client";
import { ReactNode } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
type ClientOnlyProps = { children: JSX.Element }; type ClientOnlyProps = { children: ReactNode };
const ClientOnly = (props: ClientOnlyProps) => { const ClientOnly = (props: ClientOnlyProps) => {
const { children } = props; const { children } = props;

View file

@ -3,9 +3,10 @@
import { FC } from "react"; import { FC } from "react";
import { OpenSearchWrapper } from "@link-stack/leafcutter-ui"; import { OpenSearchWrapper } from "@link-stack/leafcutter-ui";
export const Home: FC = () => ( type HomeProps = {
<OpenSearchWrapper url: string;
url="/app/visualize#/edit/237b8f00-e6a0-11ee-94b3-d7b7409294e7?embed=true" };
marginTop="0"
/> export const Home: FC<HomeProps> = ({ url }) => (
<OpenSearchWrapper url={url} margin={0} />
); );

View file

@ -501,7 +501,17 @@ export const Sidebar: FC<SidebarProps> = ({
selected={pathname.endsWith("/reporting")} selected={pathname.endsWith("/reporting")}
open={open} open={open}
/> />
{leafcutterEnabled && ( {roles.includes("admin") && leafcutterEnabled && (
<MenuItem
name="Opensearch"
href="/opensearch"
Icon={InsightsIcon}
iconSize={20}
selected={pathname.startsWith("/opensearch")}
open={open}
/>
)}
{false && leafcutterEnabled && (
<MenuItem <MenuItem
name="Leafcutter" name="Leafcutter"
href="/leafcutter" href="/leafcutter"

View file

@ -41,7 +41,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
method: "GET", method: "GET",
redirect: "manual", redirect: "manual",
}); });
console.log({ res });
if (res.type === "opaqueredirect") { if (res.type === "opaqueredirect") {
setAuthenticated(true); setAuthenticated(true);
} else { } else {
@ -69,7 +68,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
}, [session]); }, [session]);
if (!session || !authenticated) { if (!session || !authenticated) {
console.log("Not authenticated");
return ( return (
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
<Grid <Grid
@ -89,7 +87,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
} }
if (session && authenticated) { if (session && authenticated) {
console.log("Session and authenticated");
return ( return (
<Iframe <Iframe
id={id} id={id}
@ -102,10 +99,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
const linkElement = document.querySelector( const linkElement = document.querySelector(
`#${id}`, `#${id}`,
) as HTMLIFrameElement; ) as HTMLIFrameElement;
console.log({ path });
console.log({ id });
console.log({ linkElement });
if ( if (
linkElement.contentDocument && linkElement.contentDocument &&
linkElement.contentDocument?.querySelector && linkElement.contentDocument?.querySelector &&

View file

@ -1,10 +1,11 @@
import { Create } from "@link-stack/bridge-ui"; import { Create } from "@link-stack/bridge-ui";
type PageProps = { type PageProps = {
params: { segment: string[] }; params: Promise<{ segment: string[] }>;
}; };
export default function Page({ params: { segment } }: PageProps) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
return <Create service={service} />; return <Create service={service} />;

View file

@ -1,11 +1,12 @@
import { db } from "@link-stack/bridge-common"; import { db } from "@link-stack/bridge-common";
import { serviceConfig, Detail } from "@link-stack/bridge-ui"; import { serviceConfig, Detail } from "@link-stack/bridge-ui";
type Props = { type PageProps = {
params: { segment: string[] }; params: Promise<{ segment: string[] }>;
}; };
export default async function Page({ params: { segment } }: Props) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
const id = segment?.[1]; const id = segment?.[1];

View file

@ -2,10 +2,11 @@ import { db } from "@link-stack/bridge-common";
import { serviceConfig, Edit } from "@link-stack/bridge-ui"; import { serviceConfig, Edit } from "@link-stack/bridge-ui";
type PageProps = { type PageProps = {
params: { segment: string[] }; params: Promise<{ segment: string[] }>;
}; };
export default async function Page({ params: { segment } }: PageProps) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
const id = segment?.[1]; const id = segment?.[1];

View file

@ -2,12 +2,13 @@ import { db } from "@link-stack/bridge-common";
import { serviceConfig, List } from "@link-stack/bridge-ui"; import { serviceConfig, List } from "@link-stack/bridge-ui";
type PageProps = { type PageProps = {
params: { params: Promise<{
segment: string[]; segment: string[];
}; }>;
}; };
export default async function Page({ params: { segment } }: PageProps) { export default async function Page({ params }: PageProps) {
const { segment } = await params;
const service = segment[0]; const service = segment[0];
if (!service) return null; if (!service) return null;

View file

@ -1,9 +1,20 @@
"use client"; "use client";
import { useEffect } from "react";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
export default function Page() { export default function Page() {
useEffect(() => {
const multistepSignOut = async () => {
const response = await fetch("/api/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
});
signOut({ callbackUrl: "/login" }); signOut({ callbackUrl: "/login" });
};
multistepSignOut();
}, []);
return <div />; return <div />;
} }

View file

@ -0,0 +1,5 @@
import { OpenSearchWrapper } from "@link-stack/leafcutter-ui";
export default function Page() {
return <OpenSearchWrapper url="/app/visualize#/" margin={50} />;
}

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { FC, useState, useEffect } from "react"; import { FC, useState, useEffect, useActionState } from "react";
import { useFormState } from "react-dom";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { import {
@ -44,7 +43,7 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
}, },
}, },
}; };
const [formState, formAction] = useFormState( const [formState, formAction] = useActionState(
createTicketAction, createTicketAction,
initialState, initialState,
); );

View file

@ -65,7 +65,6 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
columns={gridColumns} columns={gridColumns}
onRowClick={onRowClick} onRowClick={onRowClick}
getRowID={(row: any) => { getRowID={(row: any) => {
console.log({ row });
return row.internalId; return row.internalId;
}} }}
buttons={ buttons={

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { redirect } from "next/navigation";
import { getOverviewTicketsAction } from "app/_actions/overviews"; import { getOverviewTicketsAction } from "app/_actions/overviews";
import { TicketList } from "./TicketList"; import { TicketList } from "./TicketList";
@ -12,6 +13,19 @@ type ZammadOverviewProps = {
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => { export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
const [tickets, setTickets] = useState([]); const [tickets, setTickets] = useState([]);
if (typeof window !== "undefined") {
useEffect(() => {
const hash = window?.location?.hash;
if (hash) {
const ticketID = hash.replace("#ticket/zoom/", "");
if (ticketID && !isNaN(parseInt(ticketID, 10))) {
redirect(`/tickets/${ticketID}`);
}
}
}, [window?.location?.hash]);
}
useEffect(() => { useEffect(() => {
const fetchTickets = async () => { const fetchTickets = async () => {
const { tickets } = await getOverviewTicketsAction(name); const { tickets } = await getOverviewTicketsAction(name);

View file

@ -6,14 +6,15 @@ const getSection = (overview: string) => {
}; };
type MetadataProps = { type MetadataProps = {
params: { params: Promise<{
overview: string; overview: string;
}; }>;
}; };
export async function generateMetadata({ export async function generateMetadata({
params: { overview }, params,
}: MetadataProps): Promise<Metadata> { }: MetadataProps): Promise<Metadata> {
const { overview } = await params;
const section = getSection(overview); const section = getSection(overview);
return { return {
@ -22,12 +23,13 @@ export async function generateMetadata({
} }
type PageProps = { type PageProps = {
params: { params: Promise<{
overview: string; overview: string;
}; }>;
}; };
export default function Page({ params: { overview } }: PageProps) { export default async function Page({ params }: PageProps) {
const { overview } = await params;
const section = getSection(overview); const section = getSection(overview);
return <ZammadOverview name={section} />; return <ZammadOverview name={section} />;

View file

@ -1,9 +1,10 @@
import { Metadata } from "next"; import { Metadata } from "next";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getServerSession } from "app/_lib/authentication"; import { Home } from "./_components/Home";
import { Home } from "@link-stack/leafcutter-ui"; // import { getServerSession } from "app/_lib/authentication";
import { getUserVisualizations } from "@link-stack/opensearch-common"; // import { Home } from "@link-stack/leafcutter-ui";
import { LeafcutterWrapper } from "@link-stack/leafcutter-ui"; // import { getUserVisualizations } from "@link-stack/opensearch-common";
// import { LeafcutterWrapper } from "@link-stack/leafcutter-ui";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "CDR Link - Home", title: "CDR Link - Home",
@ -11,21 +12,31 @@ export const metadata: Metadata = {
export default async function Page() { export default async function Page() {
const leafcutterEnabled = process.env.LEAFCUTTER_ENABLED === "true"; const leafcutterEnabled = process.env.LEAFCUTTER_ENABLED === "true";
const dashboardURL = process.env.LEAFCUTTER_DEFAULT_DASHBOARD_URL;
if (!leafcutterEnabled) { if (!leafcutterEnabled) {
redirect("/overview/recent"); redirect("/overview/recent");
} }
/*
const session = await getServerSession(); const session = await getServerSession();
const { const {
user: { email }, user: { email },
}: any = session; }: any = session;
*/
const visualizations = await getUserVisualizations(email ?? "none", 20); let visualizations = [];
/*
try {
visualizations = await getUserVisualizations(email ?? "none", 20);
} catch (e) {
console.error(e.meta);
}
return ( return (
<LeafcutterWrapper> <LeafcutterWrapper>
<Home visualizations={visualizations} showWelcome={false} /> <Home visualizations={visualizations} showWelcome={false} />
</LeafcutterWrapper> </LeafcutterWrapper>
); );
*/
return <Home url={dashboardURL} />;
} }

View file

@ -8,6 +8,13 @@ import { ZammadWrapper } from "app/(main)/_components/ZammadWrapper";
export const Setup: FC = () => { export const Setup: FC = () => {
const router = useRouter(); const router = useRouter();
useLayoutEffect(() => { useLayoutEffect(() => {
const fingerprint = localStorage.getItem("fingerprint");
if (!fingerprint || fingerprint === "") {
const newFingerprint = `${Math.floor(
Math.random() * 100000000,
)}`.padStart(8, "0");
localStorage.setItem("fingerprint", newFingerprint);
}
setTimeout(() => router.push("/"), 4000); setTimeout(() => router.push("/"), 4000);
}, [router]); }, [router]);

View file

@ -1,11 +1,13 @@
import { TicketDetail } from "./_components/TicketDetail"; import { TicketDetail } from "./_components/TicketDetail";
type PageProps = { type PageProps = {
params: { params: Promise<{
id: string; id: string;
}; }>;
}; };
export default function Page({ params: { id } }: PageProps) { export default async function Page({ params }: PageProps) {
const { id } = await params;
return <TicketDetail id={id} />; return <TicketDetail id={id} />;
} }

View file

@ -37,14 +37,10 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const [agents, setAgents] = useState<any>(); const [agents, setAgents] = useState<any>();
const [pendingVisible, setPendingVisible] = useState(false); const [pendingVisible, setPendingVisible] = useState(false);
const filteredStates =
ticketStates?.filter(
(state: any) => !["new", "merged", "removed"].includes(state.label),
) ?? [];
useEffect(() => { useEffect(() => {
const fetchAgents = async () => { const fetchAgents = async () => {
const result = await getAgentsAction(); const groupID = formState?.values?.group?.split("/")?.pop();
const result = await getAgentsAction(groupID);
setAgents(result); setAgents(result);
}; };
@ -67,7 +63,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
fetchTicketPriorities(); fetchTicketPriorities();
fetchAgents(); fetchAgents();
fetchGroups(); fetchGroups();
}, []); }, [formState.values.group]);
useEffect(() => { useEffect(() => {
const fetchTicket = async () => { const fetchTicket = async () => {
@ -95,7 +91,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
[name]: value, [name]: value,
}, },
}); });
const stateName = filteredStates?.find( const stateName = ticketStates?.find(
(state: any) => state.id === formState.values.state, (state: any) => state.id === formState.values.state,
)?.name; )?.name;
setPendingVisible(stateName?.includes("pending") ?? false); setPendingVisible(stateName?.includes("pending") ?? false);
@ -140,7 +136,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
label="State" label="State"
formState={formState} formState={formState}
updateFormState={updateFormState} updateFormState={updateFormState}
getOptions={() => filteredStates} getOptions={() => ticketStates}
/> />
</Grid> </Grid>
<Grid <Grid

View file

@ -1,11 +1,13 @@
import { TicketEdit } from "./_components/TicketEdit"; import { TicketEdit } from "./_components/TicketEdit";
type PageProps = { type PageProps = {
params: { params: Promise<{
id: string; id: string;
}; }>;
}; };
export default function Page({ params: { id } }: PageProps) { export default async function Page({ params }: PageProps) {
const { id } = await params;
return <TicketEdit id={id} />; return <TicketEdit id={id} />;
} }

View file

@ -1,16 +1,11 @@
"use client";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
type LayoutProps = { type LayoutProps = {
detail: any; detail: any;
edit: any; edit: any;
params: {
id: string;
};
}; };
export default function Layout({ detail, edit, params: { id } }: LayoutProps) { export default async function Layout({ detail, edit }: LayoutProps) {
return ( return (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row"> <Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}> <Grid item sx={{ height: "100vh" }} xs={9}>

View file

@ -47,10 +47,10 @@ export const getOverviewTicketsAction = async (name: string) => {
try { try {
if (name === "Recent") { if (name === "Recent") {
const recent = await executeREST({ path: "/api/v1/recent_view" }); const recent = await executeREST({ path: "/api/v1/recent_view" });
const uniqueIDs = new Set(recent.map((rec: any) => rec.o_id));
for (const rec of recent) { for (const id of uniqueIDs) {
const tkt = await executeREST({ const tkt = await executeREST({
path: `/api/v1/tickets/${rec.o_id}`, path: `/api/v1/tickets/${id}`,
}); });
tickets.push({ tickets.push({
...tkt, ...tkt,

View file

@ -1,6 +1,5 @@
"use server"; "use server";
import { revalidatePath } from "next/cache";
import { getTicketQuery } from "app/_graphql/getTicketQuery"; import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery"; import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
import { createTicketMutation } from "app/_graphql/createTicketMutation"; import { createTicketMutation } from "app/_graphql/createTicketMutation";
@ -75,7 +74,6 @@ export const updateTicketAction = async (
ticketID: string, ticketID: string,
ticketInfo: Record<string, any>, ticketInfo: Record<string, any>,
) => { ) => {
console.log({ ticketID, ticketInfo });
try { try {
const input = {}; const input = {};
if (ticketInfo.state) { if (ticketInfo.state) {
@ -158,13 +156,12 @@ export const getTicketStatesAction = async () => {
const states = await executeREST({ const states = await executeREST({
path: "/api/v1/ticket_states", path: "/api/v1/ticket_states",
}); });
const formattedStates = const formattedStates =
states?.map((state: any) => ({ states?.map((state: any) => ({
value: `gid://zammad/Ticket::State/${state.id}`, value: `gid://zammad/Ticket::State/${state.id}`,
label: state.name, label: state.name,
disabled: ["new", "merged", "removed"].includes(state.name),
})) ?? []; })) ?? [];
return formattedStates; return formattedStates;
} catch (e) { } catch (e) {
console.error(e.message); console.error(e.message);

View file

@ -2,13 +2,16 @@
import { executeREST } from "app/_lib/zammad"; import { executeREST } from "app/_lib/zammad";
export const getAgentsAction = async () => { export const getAgentsAction = async (groupID: number) => {
try { try {
const users = await executeREST({ const group = await executeREST({
path: "/api/v1/users", path: `/api/v1/groups/${groupID}`,
}); });
const { user_ids: groupUserIDs } = group;
const path = `/api/v1/users/search?query=role_ids:2&limit=1000`;
const users = await executeREST({ path });
const agents = const agents =
users?.filter((user: any) => user.role_ids.includes(2)) ?? []; users?.filter((user: any) => groupUserIDs.includes(user.id)) ?? [];
const formattedAgents = agents const formattedAgents = agents
.map((agent: any) => ({ .map((agent: any) => ({
label: `${agent.firstname} ${agent.lastname}`, label: `${agent.firstname} ${agent.lastname}`,
@ -49,7 +52,6 @@ export const getUsersAction = async () => {
const users = await executeREST({ const users = await executeREST({
path: "/api/v1/users", path: "/api/v1/users",
}); });
console.log({ users });
const formattedUsers = users const formattedUsers = users
.map((customer: any) => ({ .map((customer: any) => ({
label: customer.login, label: customer.login,

View file

@ -14,7 +14,7 @@ export const ZammadLoginProvider: FC<PropsWithChildren> = ({ children }) => {
const response = await fetch("/api/v1/users/me", { const response = await fetch("/api/v1/users/me", {
method: "GET", method: "GET",
headers: { headers: {
"X-Browser-Fingerprint": `${session.expires}`, "X-Browser-Fingerprint": localStorage.getItem("fingerprint") || "",
}, },
}); });

View file

@ -10,6 +10,7 @@ import {
import Google from "next-auth/providers/google"; import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials"; import Credentials from "next-auth/providers/credentials";
import Apple from "next-auth/providers/apple"; import Apple from "next-auth/providers/apple";
import { Redis } from "ioredis";
const headers = { Authorization: `Token ${process.env.ZAMMAD_API_TOKEN}` }; const headers = { Authorization: `Token ${process.env.ZAMMAD_API_TOKEN}` };
@ -36,6 +37,9 @@ const fetchUser = async (email: string) => {
const getUserRoles = async (email: string) => { const getUserRoles = async (email: string) => {
try { try {
const user = await fetchUser(email); const user = await fetchUser(email);
if (!user) {
return [];
}
const allRoles = await fetchRoles(); const allRoles = await fetchRoles();
const roles = user.role_ids.map((roleID: number) => { const roles = user.role_ids.map((roleID: number) => {
const role = allRoles[roleID]; const role = allRoles[roleID];
@ -43,7 +47,7 @@ const getUserRoles = async (email: string) => {
}); });
return roles.filter((role: string) => role !== null); return roles.filter((role: string) => role !== null);
} catch (e) { } catch (e) {
console.log({ e }); console.error({ e });
return []; return [];
} }
}; };
@ -109,6 +113,9 @@ export const authOptions: NextAuthOptions = {
signOut: "/logout", signOut: "/logout",
}, },
providers, providers,
session: {
maxAge: 3 * 24 * 60 * 60,
},
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET,
callbacks: { callbacks: {
signIn: async ({ user }) => { signIn: async ({ user }) => {
@ -116,6 +123,11 @@ export const authOptions: NextAuthOptions = {
return roles.includes("admin") || roles.includes("agent"); return roles.includes("admin") || roles.includes("agent");
}, },
session: async ({ session, token }) => { session: async ({ session, token }) => {
const redis = new Redis(process.env.REDIS_URL);
const isInvalidated = await redis.get(`invalidated:${token.sub}`);
if (isInvalidated) {
return null;
}
// @ts-ignore // @ts-ignore
session.user.roles = token.roles ?? []; session.user.roles = token.roles ?? [];
// @ts-ignore // @ts-ignore

View file

@ -13,21 +13,18 @@ export const fetchLeafcutter = async (url: string, options: any) => {
const json = await res.json(); const json = await res.json();
return json; return json;
} catch (error) { } catch (error) {
console.log({ error }); console.error({ error });
return null; return null;
} }
}; };
const data = await fetchData(url, options); const data = await fetchData(url, options);
console.log({ data });
if (!data) { if (!data) {
const csrfURL = `${process.env.NEXT_PUBLIC_LEAFCUTTER_URL}/api/auth/csrf`; const csrfURL = `${process.env.NEXT_PUBLIC_LEAFCUTTER_URL}/api/auth/csrf`;
const csrfData = await fetchData(csrfURL, {}); const csrfData = await fetchData(csrfURL, {});
console.log({ csrfData });
const authURL = `${process.env.NEXT_PUBLIC_LEAFCUTTER_URL}/api/auth/callback/credentials`; const authURL = `${process.env.NEXT_PUBLIC_LEAFCUTTER_URL}/api/auth/callback/credentials`;
const authData = await fetchData(authURL, { method: "POST" }); const authData = await fetchData(authURL, { method: "POST" });
console.log({ authData });
if (!authData) { if (!authData) {
return null; return null;
} else { } else {
@ -37,5 +34,3 @@ export const fetchLeafcutter = async (url: string, options: any) => {
return data; return data;
} }
}; };

View file

@ -2,12 +2,11 @@ import { getServerSession } from "app/_lib/authentication";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
const getHeaders = async () => { const getHeaders = async () => {
const allCookies = cookies().getAll(); const allCookies = (await cookies()).getAll();
const session = await getServerSession(); const session = await getServerSession();
const headers = { const finalHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
"X-Browser-Fingerprint": `${session.expires}`,
// @ts-ignore // @ts-ignore
"X-CSRF-Token": session.user.zammadCsrfToken, "X-CSRF-Token": session.user.zammadCsrfToken,
Cookie: allCookies Cookie: allCookies
@ -15,7 +14,7 @@ const getHeaders = async () => {
.join("; "), .join("; "),
}; };
return headers; return finalHeaders;
}; };
interface ExecuteGraphQLOptions { interface ExecuteGraphQLOptions {

View file

@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from "next/server";
import { Redis } from "ioredis";
import { getToken } from "next-auth/jwt";
export async function POST(request: NextRequest) {
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
});
const allCookies = request.cookies.getAll();
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
const signOutURL = `${zammadURL}/api/v1/signout`;
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
Cookie: allCookies
.map((cookie) => `${cookie.name}=${cookie.value}`)
.join("; "),
};
await fetch(signOutURL, { headers });
const cookiePrefixesToRemove = ["_zammad"];
const response = NextResponse.json({ message: "ok" });
for (const cookie of allCookies) {
if (
cookiePrefixesToRemove.some((prefix) => cookie.name.startsWith(prefix))
) {
response.cookies.set(cookie.name, "", { path: "/", maxAge: 0 });
}
}
const redis = new Redis(process.env.REDIS_URL);
await redis.setex(`invalidated:${token.sub}`, 24 * 60 * 60, "1");
return response;
}

View file

@ -12,9 +12,11 @@ const rewriteURL = (
path = path.slice(1); path = path.slice(1);
} }
const destinationURL = `${destinationBaseURL}/${path}`; const destinationURL = `${destinationBaseURL}/${path}`;
console.log(`Rewriting ${request.url} to ${destinationURL}`); console.info(`Rewriting ${request.url} to ${destinationURL}`);
const requestHeaders = new Headers(request.headers); const requestHeaders = new Headers(request.headers);
requestHeaders.delete("x-forwarded-user"); requestHeaders.delete("x-forwarded-user");
requestHeaders.delete("x-forwarded-roles");
requestHeaders.delete("connection"); requestHeaders.delete("connection");
for (const [key, value] of Object.entries(headers)) { for (const [key, value] of Object.entries(headers)) {
@ -29,6 +31,9 @@ const rewriteURL = (
const checkRewrites = async (request: NextRequestWithAuth) => { const checkRewrites = async (request: NextRequestWithAuth) => {
const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000"; const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080"; const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
const opensearchBaseURL =
process.env.OPENSEARCH_DASHBOARDS_URL ??
"http://opensearch-dashboards:5601";
const zammadPaths = [ const zammadPaths = [
"/zammad", "/zammad",
@ -38,19 +43,38 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
"/graphql", "/graphql",
"/cable", "/cable",
]; ];
const isSetupMode = process.env.SETUP_MODE === "true";
const { token } = request.nextauth; const { token } = request.nextauth;
const email = token?.email?.toLowerCase() ?? "unknown"; const email = token?.email?.toLowerCase() ?? "unknown";
let headers = { "x-forwarded-user": email }; const roles = (token?.roles as string[]) ?? [];
let headers = {
"x-forwarded-user": email,
"x-forwarded-roles": roles.join(","),
};
if (request.nextUrl.pathname.startsWith("/zammad")) { if (request.nextUrl.pathname.startsWith("/dashboards")) {
return rewriteURL(
request,
`${linkBaseURL}/dashboards`,
opensearchBaseURL,
headers,
);
} else if (request.nextUrl.pathname.startsWith("/zammad")) {
return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers); return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers);
} else if (zammadPaths.some((p) => request.nextUrl.pathname.startsWith(p))) { } else if (zammadPaths.some((p) => request.nextUrl.pathname.startsWith(p))) {
return rewriteURL(request, linkBaseURL, zammadURL, headers); return rewriteURL(request, linkBaseURL, zammadURL, headers);
} else if (request.nextUrl.pathname.startsWith("/api/v1")) {
if ((email && email !== "unknown") || isSetupMode) {
return NextResponse.next();
} else {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
} else { } else {
const isDev = process.env.NODE_ENV === "development"; const isDev = process.env.NODE_ENV === "development";
const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = ` const cspHeader = `
default-src 'self'; default-src 'self';
frame-src 'self' https://digiresilience.org;
connect-src 'self'; connect-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""}; script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""};
style-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
@ -59,7 +83,7 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
object-src 'none'; object-src 'none';
base-uri 'self'; base-uri 'self';
form-action 'self'; form-action 'self';
frame-ancestors 'none'; frame-ancestors 'self';
upgrade-insecure-requests; upgrade-insecure-requests;
`; `;
const contentSecurityPolicyHeaderValue = cspHeader const contentSecurityPolicyHeaderValue = cspHeader
@ -116,5 +140,7 @@ export default withAuth(checkRewrites, {
}); });
export const config = { export const config = {
matcher: ["/((?!ws|wss|api|_next/static|_next/image|favicon.ico).*)"], matcher: [
"/((?!ws|wss|api/signal|api/whatsapp|_next/static|_next/image|favicon.ico).*)",
],
}; };

View file

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -20,7 +20,7 @@ const nextConfig = {
}, },
{ {
key: "X-Frame-Options", key: "X-Frame-Options",
value: "DENY", value: "SAMEORIGIN",
}, },
{ {
key: "X-Content-Type-Options", key: "X-Content-Type-Options",

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack/link", "name": "@link-stack/link",
"version": "2.2.0", "version": "2.4.0b1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
@ -12,38 +12,39 @@
"dependencies": { "dependencies": {
"@chatscope/chat-ui-kit-react": "^2.0.3", "@chatscope/chat-ui-kit-react": "^2.0.3",
"@chatscope/chat-ui-kit-styles": "^1.4.0", "@chatscope/chat-ui-kit-styles": "^1.4.0",
"@emotion/cache": "^11.13.1", "@emotion/cache": "^11.14.0",
"@emotion/react": "^11.13.3", "@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.14.0",
"@link-stack/bridge-common": "*", "@link-stack/bridge-common": "*",
"@link-stack/bridge-ui": "*", "@link-stack/bridge-ui": "*",
"@link-stack/leafcutter-ui": "*", "@link-stack/leafcutter-ui": "*",
"@link-stack/opensearch-common": "*", "@link-stack/opensearch-common": "*",
"@link-stack/ui": "*", "@link-stack/ui": "*",
"@mui/icons-material": "^5", "@mui/icons-material": "^6",
"@mui/material": "^5", "@mui/material": "^6",
"@mui/material-nextjs": "^5", "@mui/material-nextjs": "^6",
"@mui/x-data-grid-pro": "^7.18.0", "@mui/x-data-grid-pro": "^7.24.1",
"@mui/x-date-pickers": "^7.18.0", "@mui/x-date-pickers": "^7.24.1",
"@mui/x-date-pickers-pro": "^7.18.0", "@mui/x-date-pickers-pro": "^7.24.1",
"@mui/x-license": "^7.18.0", "@mui/x-license": "^7.24.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"graphql-request": "^7.1.0", "graphql-request": "^7.1.2",
"mui-chips-input": "^2.1.5", "ioredis": "^5.4.2",
"next": "14.2.13", "mui-chips-input": "^4.0.1",
"next-auth": "^4.24.8", "next": "15.1.6",
"react": "18.3.1", "next-auth": "^4.24.11",
"react-cookie": "^7.2.0", "react": "19.0.0",
"react-dom": "18.3.1", "react-cookie": "^7.2.2",
"react-dom": "19.0.0",
"react-iframe": "^1.8.5", "react-iframe": "^1.8.5",
"react-polyglot": "^0.7.2", "react-polyglot": "^0.7.2",
"sharp": "^0.33.5" "sharp": "^0.33.5"
}, },
"devDependencies": { "devDependencies": {
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@types/node": "^22.7.3", "@types/node": "^22.10.10",
"@types/react": "18.3.9", "@types/react": "19.0.8",
"@types/uuid": "^10.0.0" "@types/uuid": "^10.0.0"
} }
} }

Binary file not shown.

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -25,6 +25,7 @@ services:
context: ../../ context: ../../
dockerfile: ./apps/bridge-frontend/Dockerfile dockerfile: ./apps/bridge-frontend/Dockerfile
container_name: bridge-frontend container_name: bridge-frontend
image: registry.gitlab.com/digiresilience/link/link-stack/bridge-frontend:${LINK_STACK_VERSION}
restart: ${RESTART} restart: ${RESTART}
ports: ports:
- 8006:3000 - 8006:3000
@ -35,5 +36,6 @@ services:
context: ../../ context: ../../
dockerfile: ./apps/bridge-worker/Dockerfile dockerfile: ./apps/bridge-worker/Dockerfile
container_name: bridge-worker container_name: bridge-worker
image: registry.gitlab.com/digiresilience/link/link-stack/bridge-worker:${LINK_STACK_VERSION}
restart: ${RESTART} restart: ${RESTART}
environment: *common-bridge-variables environment: *common-bridge-variables

View file

@ -17,6 +17,7 @@ services:
LEAFCUTTER_URL: https://lc.digiresilience.org LEAFCUTTER_URL: https://lc.digiresilience.org
BRIDGE_URL: http://bridge-frontend:3000 BRIDGE_URL: http://bridge-frontend:3000
ZAMMAD_URL: http://zammad-nginx:8080 ZAMMAD_URL: http://zammad-nginx:8080
REDIS_URL: "redis://zammad-redis:6379"
NEXTAUTH_URL: ${LINK_URL} NEXTAUTH_URL: ${LINK_URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE} NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE}

View file

@ -6,8 +6,8 @@ services:
restart: ${RESTART} restart: ${RESTART}
environment: environment:
- discovery.type=single-node - discovery.type=single-node
# - plugins.security.ssl.transport.enforce_hostname_verification=false - plugins.security.ssl.transport.enforce_hostname_verification=false
# - plugins.security.ssl.transport.resolve_hostname=false - plugins.security.ssl.transport.resolve_hostname=false
- cluster.routing.allocation.disk.watermark.low=3gb - cluster.routing.allocation.disk.watermark.low=3gb
- cluster.routing.allocation.disk.watermark.high=2gb - cluster.routing.allocation.disk.watermark.high=2gb
- cluster.routing.allocation.disk.watermark.flood_stage=500mb - cluster.routing.allocation.disk.watermark.flood_stage=500mb
@ -47,10 +47,10 @@ services:
- ../opensearch-dashboards/opensearch_dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml - ../opensearch-dashboards/opensearch_dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
environment: environment:
OPENSEARCH_HOSTS: '["https://opensearch:9200"]' OPENSEARCH_HOSTS: '["https://opensearch:9200"]'
# OPENSEARCH_SECURITY_AUTH_TYPE: "proxy" OPENSEARCH_SECURITY_AUTH_TYPE: "proxy"
# OPENSEARCH_SECURITY_PROXYCACHE_USER_HEADER: "x-proxy-user" OPENSEARCH_SECURITY_PROXYCACHE_USER_HEADER: "x-proxy-user"
# OPENSEARCH_SECURITY_PROXYCACHE_ROLES_HEADER: "x-proxy-roles" OPENSEARCH_SECURITY_PROXYCACHE_ROLES_HEADER: "x-proxy-roles"
# OPENSEARCH_REQUESTHEADERSALLOWLIST: '["securitytenant","Authorization","x-forwarded-for","x-proxy-user","x-proxy-roles"]' OPENSEARCH_REQUESTHEADERSALLOWLIST: '["securitytenant","Authorization","x-forwarded-for","x-proxy-user","x-proxy-roles"]'
volumes: volumes:
opensearch-data: opensearch-data:

View file

@ -1,7 +1,8 @@
services: services:
signal-cli-rest-api: signal-cli-rest-api:
container_name: signal-cli-rest-api
build: ../signal-cli-rest-api
image: registry.gitlab.com/digiresilience/link/link-stack/signal-cli-rest-api:develop image: registry.gitlab.com/digiresilience/link/link-stack/signal-cli-rest-api:develop
platform: linux/amd64
environment: environment:
- MODE=json-rpc - MODE=json-rpc
volumes: volumes:

View file

@ -17,7 +17,6 @@ x-zammad-vars: &common-zammad-variables
services: services:
zammad-init: zammad-init:
platform: linux/x86_64
container_name: zammad-init container_name: zammad-init
command: ["zammad-init"] command: ["zammad-init"]
depends_on: depends_on:
@ -47,7 +46,6 @@ services:
<<: *common-global-variables <<: *common-global-variables
zammad-nginx: zammad-nginx:
platform: linux/x86_64
container_name: zammad-nginx container_name: zammad-nginx
command: ["zammad-nginx"] command: ["zammad-nginx"]
expose: expose:
@ -72,7 +70,6 @@ services:
- zammad-var:/opt/zammad/var:ro - zammad-var:/opt/zammad/var:ro
zammad-railsserver: zammad-railsserver:
platform: linux/x86_64
container_name: zammad-railsserver container_name: zammad-railsserver
command: ["zammad-railsserver"] command: ["zammad-railsserver"]
depends_on: depends_on:
@ -103,7 +100,6 @@ services:
- redis-data:/data - redis-data:/data
zammad-scheduler: zammad-scheduler:
platform: linux/x86_64
container_name: zammad-scheduler container_name: zammad-scheduler
command: ["zammad-scheduler"] command: ["zammad-scheduler"]
depends_on: depends_on:
@ -123,7 +119,6 @@ services:
- zammad-storage:/opt/zammad/storage - zammad-storage:/opt/zammad/storage
zammad-websocket: zammad-websocket:
platform: linux/x86_64
container_name: zammad-websocket container_name: zammad-websocket
command: ["zammad-websocket"] command: ["zammad-websocket"]
depends_on: depends_on:

View file

@ -1 +1 @@
FROM memcached:1.6.31-bookworm FROM memcached:1.6.34-bookworm

View file

@ -1 +1 @@
FROM nginxproxy/nginx-proxy:1.6.1 FROM nginxproxy/nginx-proxy:1.6.4

View file

@ -1 +1 @@
FROM opensearchproject/opensearch-dashboards:2.17.0 FROM opensearchproject/opensearch-dashboards:2.18.0

View file

@ -1,2 +1,2 @@
FROM opensearchproject/opensearch:2.17.0 FROM opensearchproject/opensearch:2.18.0
RUN /usr/share/opensearch/bin/opensearch-plugin install ingest-attachment -b RUN /usr/share/opensearch/bin/opensearch-plugin install ingest-attachment -b

View file

@ -1 +1 @@
FROM redis:7.4.0-bookworm FROM redis:7.4.2-bookworm

View file

@ -17,14 +17,13 @@ const files = {
const finalFiles = files[app] const finalFiles = files[app]
.map((file) => ['-f', `docker/compose/${file}.yml`]).flat(); .map((file) => ['-f', `docker/compose/${file}.yml`]).flat();
const finalCommand = command === "up" ? ["up", "-d"] : [command]; const finalCommand = command === "up" ? ["up", "-d", "--remove-orphans"] : [command];
const dockerCompose = spawn('docker', ['compose', '--env-file', '.env', ...finalFiles, ...finalCommand]); const dockerCompose = spawn('docker', ['compose', '--env-file', '.env', ...finalFiles, ...finalCommand]);
dockerCompose.stdout.on('data', (data) => { dockerCompose.stdout.on('data', (data) => {
console.log(`${data}`); console.info(`${data}`);
}); });
dockerCompose.stderr.on('data', (data) => { dockerCompose.stderr.on('data', (data) => {
console.log(`${data}`); console.info(`${data}`);
}); });

View file

@ -1 +1 @@
FROM bbernhard/signal-cli-rest-api:0.89 FROM bbernhard/signal-cli-rest-api:0.90

View file

@ -1,19 +1,25 @@
ARG ZAMMAD_VERSION=6.3.1 ARG ZAMMAD_VERSION=6.4.1
FROM node:22-slim as node FROM node:22-slim AS node
FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder
USER root
COPY --from=node /opt /opt COPY --from=node /opt /opt
COPY --from=node /usr/local/bin /usr/local/bin COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /usr/local/lib /usr/local/lib
COPY --from=node /usr/lib /usr/lib
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
RUN npm install -g pnpm
RUN pnpm --version
WORKDIR ${ZAMMAD_DIR} WORKDIR ${ZAMMAD_DIR}
RUN mkdir -p /opt/zammad/contrib/link/addons RUN mkdir -p /opt/zammad/contrib/link/addons
COPY addons contrib/link/addons COPY addons contrib/link/addons
COPY setup.rb contrib/link/setup.rb COPY setup.rb contrib/link/setup.rb
COPY install.rb contrib/link/install.rb COPY install.rb contrib/link/install.rb
USER root
RUN sed -i '/script\/build\/cleanup\.sh/d' contrib/docker/setup.sh RUN sed -i '/script\/build\/cleanup\.sh/d' contrib/docker/setup.sh
RUN sed -i '/touch db\/schema.rb/a ZAMMAD_SAFE_MODE=1 DATABASE_URL=postgresql:\/\/zammad:\/zammad bundle exec rails runner \/opt\/zammad\/contrib\/link\/install.rb' contrib/docker/setup.sh
RUN cat contrib/docker/setup.sh
RUN contrib/docker/setup.sh builder RUN contrib/docker/setup.sh builder
ARG EMBEDDED=false ARG EMBEDDED=false
RUN if [ "$EMBEDDED" = "true" ] ; then sed -i '/proxy_set_header X-Forwarded-User "";/d' ${ZAMMAD_DIR}/contrib/nginx/zammad.conf; fi RUN if [ "$EMBEDDED" = "true" ] ; then sed -i '/proxy_set_header X-Forwarded-User "";/d' ${ZAMMAD_DIR}/contrib/nginx/zammad.conf; fi
@ -22,11 +28,9 @@ RUN sed -i '/^[[:space:]]*# es config/a\
bundle exec rails runner /opt/zammad/contrib/link/setup.rb\n\ bundle exec rails runner /opt/zammad/contrib/link/setup.rb\n\
bundle exec rake zammad:package:migrate\n\ bundle exec rake zammad:package:migrate\n\
' /docker-entrypoint.sh ' /docker-entrypoint.sh
RUN ZAMMAD_SAFE_MODE=1 DATABASE_URL=postgresql://zammad:/zammad bundle exec rails runner /opt/zammad/contrib/link/install.rb
RUN ZAMMAD_SAFE_MODE=1 DATABASE_URL=postgresql://zammad:/zammad bundle exec rake assets:precompile
FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} as runner FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS runner
USER ${ZAMMAD_USER} USER zammad
COPY --from=builder --chown=zammad:zammad ${ZAMMAD_DIR} ${ZAMMAD_DIR} COPY --from=builder --chown=zammad:zammad ${ZAMMAD_DIR} ${ZAMMAD_DIR}
COPY --from=builder /usr/local/bundle /usr/local/bundle COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /docker-entrypoint.sh /docker-entrypoint.sh COPY --from=builder /docker-entrypoint.sh /docker-entrypoint.sh

6509
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,13 @@
{ {
"name": "@link-stack", "name": "@link-stack",
"version": "2.2.0", "version": "2.4.0b1",
"description": "Link from the Center for Digital Resilience", "description": "Link from the Center for Digital Resilience",
"scripts": { "scripts": {
"dev": "dotenv -- turbo dev", "dev": "dotenv -- turbo dev",
"build": "dotenv -- turbo build", "build": "dotenv -- turbo build",
"migrate": "dotenv -- npm run migrate --workspace=database", "migrate": "dotenv -- npm run migrate --workspace=database",
"lint": "dotenv turbo lint", "lint": "dotenv turbo lint",
"update-version": "find . -name 'package.json' -exec sed -i -E 's/\"version\": \"[^\"]+\"/\"version\": \"2.2.0\"/' {} +", "update-version": "find . -name 'package.json' -exec sed -i -E 's/\"version\": \"[^\"]+\"/\"version\": \"2.4.0b1\"/' {} +",
"upgrade:setup": "npm i -g npm-check-updates", "upgrade:setup": "npm i -g npm-check-updates",
"upgrade:check": "ncu && ncu -ws", "upgrade:check": "ncu && ncu -ws",
"upgrade": "ncu -u -x eslint -x kysely && ncu -ws -u -x eslint -x kysely && npm i", "upgrade": "ncu -u -x eslint -x kysely && ncu -ws -u -x eslint -x kysely && npm i",
@ -46,39 +46,33 @@
"type": "git", "type": "git",
"url": "git+https://gitlab.com/digiresilience/link/link-stack.git" "url": "git+https://gitlab.com/digiresilience/link/link-stack.git"
}, },
"packageManager": "npm@10.8.3", "packageManager": "npm@11.0.0",
"author": "Darren Clarke", "author": "Darren Clarke",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"dotenv-cli": "latest", "dotenv-cli": "latest",
"eslint": "^8", "eslint": "^9",
"turbo": "^2.1.2", "turbo": "^2.3.4",
"typescript": "latest" "typescript": "latest",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@types/react": "^19.0.8"
}, },
"overrides": { "overrides": {
"eslint-config-next": { "material-ui-popup-state": {
"eslint": "$eslint" "react": "$react"
}, },
"eslint-plugin-import": { "@chatscope/chat-ui-kit-react": {
"eslint": "$eslint" "react": "$react",
"react-dom": "$react-dom"
}, },
"eslint-plugin-jsx-a11y": { "@mui/icons-material": {
"eslint": "$eslint" "react": "$react"
}, },
"eslint-plugin-react": { "mui-chips-input": {
"eslint": "$eslint" "react": "$react",
}, "react-dom": "$react-dom",
"@typescript-eslint/eslint-plugin": { "@types/react": "$@types/react"
"eslint": "$eslint"
},
"@typescript-eslint/parser": {
"eslint": "$eslint"
},
"eslint-plugin-promise": {
"eslint": "$eslint"
},
"@babel/eslint-parser": {
"eslint": "$eslint"
} }
}, },
"engines": { "engines": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@link-stack/bridge-common", "name": "@link-stack/bridge-common",
"version": "2.2.0", "version": "2.4.0b1",
"main": "build/main/index.js", "main": "build/main/index.js",
"type": "module", "type": "module",
"author": "Darren Clarke <darren@redaranj.com>", "author": "Darren Clarke <darren@redaranj.com>",
@ -9,14 +9,14 @@
"build": "tsc -p tsconfig.json" "build": "tsc -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@auth/kysely-adapter": "^1.5.2", "@auth/kysely-adapter": "^1.7.4",
"graphile-worker": "^0.16.6", "graphile-worker": "^0.16.6",
"kysely": "0.26.1", "kysely": "0.26.1",
"pg": "^8.13.0" "pg": "^8.13.1"
}, },
"devDependencies": { "devDependencies": {
"@link-stack/eslint-config": "*", "@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*", "@link-stack/typescript-config": "*",
"typescript": "^5.6.2" "typescript": "^5.7.3"
} }
} }

View file

@ -30,7 +30,6 @@ export const QRCode: FC<QRCodeProps> = ({
if (!verified && getValue && refreshInterval) { if (!verified && getValue && refreshInterval) {
const interval = setInterval(async () => { const interval = setInterval(async () => {
const { qr, kind } = await getValue(token); const { qr, kind } = await getValue(token);
console.log({ kind });
setValue(qr); setValue(qr);
setKind(kind); setKind(kind);
}, refreshInterval * 1000); }, refreshInterval * 1000);

View file

@ -3,18 +3,19 @@ type ServiceLayoutProps = {
detail: any; detail: any;
edit: any; edit: any;
create: any; create: any;
params: { params: Promise<{
segment: string[]; segment: string[];
}; }>;
}; };
export const ServiceLayout = ({ export const ServiceLayout = async ({
children, children,
detail, detail,
edit, edit,
create, create,
params: { segment }, params,
}: ServiceLayoutProps) => { }: ServiceLayoutProps) => {
const { segment } = await params;
const length = segment?.length ?? 0; const length = segment?.length ?? 0;
const isCreate = length === 2 && segment[1] === "create"; const isCreate = length === 2 && segment[1] === "create";
const isEdit = length === 3 && segment[2] === "edit"; const isEdit = length === 3 && segment[2] === "edit";

View file

@ -5,19 +5,21 @@ import { getService } from "./utils";
export const getBot = async ( export const getBot = async (
_req: NextRequest, _req: NextRequest,
params: ServiceParams, params: ServiceParams,
): Promise<NextResponse> => getService(params)?.getBot(params); ): Promise<NextResponse> => (await getService(params))?.getBot(params);
export const sendMessage = async ( export const sendMessage = async (
req: NextRequest, req: NextRequest,
params: ServiceParams, params: ServiceParams,
): Promise<NextResponse> => getService(params)?.sendMessage(req, params); ): Promise<NextResponse> =>
(await getService(params))?.sendMessage(req, params);
export const receiveMessage = async ( export const receiveMessage = async (
req: NextRequest, req: NextRequest,
params: ServiceParams, params: ServiceParams,
): Promise<NextResponse> => getService(params)?.receiveMessage(req, params); ): Promise<NextResponse> =>
(await getService(params))?.receiveMessage(req, params);
export const handleWebhook = async ( export const handleWebhook = async (
req: NextRequest, req: NextRequest,
params: ServiceParams, params: ServiceParams,
): Promise<NextResponse> => getService(params)?.handleWebhook(req); ): Promise<NextResponse> => (await getService(params))?.handleWebhook(req);

Some files were not shown because too many files have changed in this diff Show more