Merge branch 'main' into shell-updates

This commit is contained in:
Darren Clarke 2023-06-14 06:02:11 +00:00 committed by GitHub
commit db8a3d1ee0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
132 changed files with 3609 additions and 5150 deletions

View file

@ -175,7 +175,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const urgentCount = findOverviewCountByID(7);
const pendingCount = findOverviewCountByID(3);
const unassignedCount = findOverviewCountByID(2);
console.log({ assignedCount, urgentCount, pendingCount, unassignedCount });
const logout = () => {
signOut({ callbackUrl: "/login" });

View file

@ -1,60 +0,0 @@
FROM node:20-bullseye as builder
ARG METAMIGO_DIR=/opt/metamigo
RUN mkdir -p ${METAMIGO_DIR}/
WORKDIR ${METAMIGO_DIR}
COPY package.json tsconfig.json ${METAMIGO_DIR}/
COPY . ${METAMIGO_DIR}/
RUN npm install
RUN npm run build
# RUN npx --no-install tsc --build --verbose
RUN rm -Rf ./node_modules
FROM node:20-bullseye as clean
ARG METAMIGO_DIR=/opt/metamigo
COPY --from=builder ${METAMIGO_DIR} ${METAMIGO_DIR}/
RUN rm -Rf ./node_modules
FROM node:20-bullseye as pristine
LABEL maintainer="Abel Luck <abel@guardianproject.info>"
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends --fix-missing \
postgresql-client dumb-init ffmpeg
ARG METAMIGO_DIR=/opt/metamigo
ENV METAMIGO_DIR ${METAMIGO_DIR}
RUN mkdir -p ${METAMIGO_DIR}
RUN chown -R node:node ${METAMIGO_DIR}/
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
COPY --from=clean ${METAMIGO_DIR}/ ${METAMIGO_DIR}/
WORKDIR ${METAMIGO_DIR}
USER node
EXPOSE 3000
EXPOSE 3001
EXPOSE 3002
ENV PORT 3000
ENV NODE_ENV production
ARG BUILD_DATE
ARG VCS_REF
ARG VCS_URL="https://gitlab.com/digiresilience/link/metamigo"
ARG VERSION
LABEL org.label-schema.schema-version="1.0"
LABEL org.label-schema.name="digiresilience.org/link/metamigo"
LABEL org.label-schema.description="part of CDR Link"
LABEL org.label-schema.build-date=$BUILD_DATE
LABEL org.label-schema.vcs-url=$VCS_URL
LABEL org.label-schema.vcs-ref=$VCS_REF
LABEL org.label-schema.version=$VERSION
ENTRYPOINT ["/docker-entrypoint.sh"]

View file

@ -1,23 +0,0 @@
#!/bin/bash
set -e
cd ${AMIGO_DIR}
if [[ "$1" == "api" ]]; then
echo "docker-entrypoint: starting api server"
./cli db -- migrate
exec dumb-init ./cli api
elif [[ "$1" == "worker" ]]; then
echo "docker-entrypoint: starting worker"
exec dumb-init ./cli worker
elif [[ "$1" == "frontend" ]]; then
echo "docker-entrypoint: starting frontend"
exec dumb-init yarn workspace @app/frontend start
elif [[ "$1" == "cli" ]]; then
echo "docker-entrypoint: starting frontend"
shift 1
exec ./cli "$@"
else
echo "docker-entrypoint: missing argument, one of: api, worker, frontend, cli"
exit 1
fi

View file

@ -1,7 +1,8 @@
{
"name": "metamigo-api",
"name": "@digiresilience/metamigo-api",
"version": "0.2.0",
"main": "build/main/cli/index.js",
"type": "module",
"main": "build/main/main.js",
"author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later",
"dependencies": {
@ -26,8 +27,8 @@
"fluent-ffmpeg": "^2.1.2",
"graphile-migrate": "^1.4.1",
"graphile-worker": "^0.13.0",
"hapi-auth-bearer-token": "^8.0.0",
"hapi-auth-jwt2": "^10.4.0",
"hapi-postgraphile": "^0.11.0",
"hapi-swagger": "^16.0.1",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.0",
@ -37,6 +38,7 @@
"pg": "^8.11.0",
"pg-monitor": "^2.0.0",
"pg-promise": "^11.4.3",
"postgraphile": "4.12.3",
"postgraphile-plugin-connection-filter": "^2.3.0",
"remeda": "^1.18.1",
"twilio": "^4.11.1",
@ -53,6 +55,7 @@
"pg-monitor": "^2.0.0",
"pino-pretty": "^10.0.0",
"ts-node": "^10.9.1",
"tsc-watch": "^6.0.4",
"tsconfig-link": "*",
"typedoc": "^0.24.7",
"typescript": "^5.0.4"
@ -75,6 +78,7 @@
"serve:prod": "NODE_ENV=production npm run cli server",
"worker": "NODE_ENV=development npm run cli worker",
"worker:prod": "NODE_ENV=production npm run cli worker",
"watch:build": "tsc -p tsconfig.json -w"
"watch:build": "tsc -p tsconfig.json -w",
"dev": "tsc-watch --build --noClear --onSuccess \"node ./build/main/main.js\""
}
}

View file

@ -1,9 +1,9 @@
import type * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import type { IAppConfig } from "../config";
import * as Services from "./services";
import * as Routes from "./routes";
import * as Plugins from "./plugins";
import Joi from "joi";
import type { IAppConfig } from "../config.js";
import * as Services from "./services/index.js";
import * as Routes from "./routes/index.js";
import * as Plugins from "./plugins/index.js";
const AppPlugin = {
name: "App",

View file

@ -0,0 +1,28 @@
import type * as Hapi from "@hapi/hapi";
import AuthBearer from "hapi-auth-bearer-token";
import { IAppConfig } from "@digiresilience/metamigo-config";
import { IMetamigoRepositories } from "@digiresilience/metamigo-common";
export const registerAuthBearer = async (
server: Hapi.Server,
config: IAppConfig
): Promise<void> => {
await server.register(AuthBearer);
server.auth.strategy("session-id-bearer-token", "bearer-access-token", {
allowQueryToken: false,
validate: async (
request: Hapi.Request,
token: string,
h: Hapi.ResponseToolkit
) => {
const repos = request.db() as IMetamigoRepositories;
const session = await repos.sessions.findBy({ sessionToken: token });
const isValid = !!session;
if (!isValid) return { isValid, credentials: {} };
const user = await repos.users.findById({ id: session.userId });
const credentials = { sessionToken: token, user };
return { isValid, credentials };
},
});
};

View file

@ -7,7 +7,8 @@ export const registerNextAuth = async (
server: Hapi.Server,
config: IAppConfig
): Promise<void> => {
const nextAuthAdapterFactory: any = (request: Hapi.Request) => new NextAuthAdapter(request.db());
const nextAuthAdapterFactory: any = (request: Hapi.Request) =>
new NextAuthAdapter(request.db());
await server.register({
plugin: NextAuthPlugin,

View file

@ -0,0 +1,71 @@
import type * as Hapi from "@hapi/hapi";
import { IAppConfig } from "@digiresilience/metamigo-config";
import { postgraphile, HttpRequestHandler } from "postgraphile";
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
export interface HapiPostgraphileOptions {}
const PostgraphilePlugin: Hapi.Plugin<HapiPostgraphileOptions> = {
name: "postgraphilePlugin",
version: "1.0.0",
register: async function (server, options: HapiPostgraphileOptions) {
const config = server.config();
const postgraphileMiddleware: HttpRequestHandler = postgraphile(
config.postgraphile.authConnection,
"app_public",
{
...getPostGraphileOptions(),
jwtSecret: "",
pgSettings: async (req) => {
const auth = (req as any).hapiAuth;
if (auth.isAuthenticated && auth.credentials.user.userRole) {
return {
role: `app_${auth.credentials.user.userRole}`,
"jwt.claims.session_id": auth.credentials.sessionToken,
};
} else {
return {
role: "app_anonymous",
};
}
},
}
);
server.route({
method: ["POST"],
path: "/graphql",
options: {
auth: "session-id-bearer-token",
payload: {
parse: false, // this disables payload parsing
output: "stream", // ensures the payload is a readable stream which postgraphile expects
},
},
handler: (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
return new Promise((resolve, reject) => {
const rawReq = request.raw.req as any;
rawReq.hapiAuth = request.auth;
postgraphileMiddleware(rawReq, request.raw.res, (error) => {
if (error) {
reject(error);
} else {
// PostGraphile responds directly to the request
resolve(h.abandon);
}
});
});
},
});
},
};
export const registerPostgraphile = async (
server: Hapi.Server,
config: IAppConfig
): Promise<void> => {
await server.register({
plugin: PostgraphilePlugin,
options: {},
});
};

View file

@ -5,12 +5,14 @@ import { makePlugin } from "@digiresilience/hapi-pg-promise";
import type { IAppConfig } from "../../config";
import { dbInitOptions, IRepositories } from "@digiresilience/metamigo-db";
import { registerNextAuth } from "./hapi-nextauth";
import { registerSwagger } from "./swagger";
import { registerNextAuthJwt } from "./nextauth-jwt";
import { registerCloudflareAccessJwt } from "./cloudflare-jwt";
import { registerNextAuth } from "./hapi-nextauth.js";
import { registerSwagger } from "./swagger.js";
import { registerCloudflareAccessJwt } from "./cloudflare-jwt.js";
import { registerAuthBearer } from "./auth-bearer.js";
import pg from "pg-promise/typescript/pg-subset";
import { registerPostgraphile } from "./hapi-postgraphile.js";
export const register = async (
server: Hapi.Server,
config: IAppConfig
@ -34,6 +36,7 @@ export const register = async (
await registerNextAuth(server, config);
await registerSwagger(server);
await registerNextAuthJwt(server, config);
await registerCloudflareAccessJwt(server, config);
await registerAuthBearer(server, config);
await registerPostgraphile(server, config);
};

View file

@ -1,104 +0,0 @@
import * as Hoek from "@hapi/hoek";
import * as Hapi from "@hapi/hapi";
import type { IAppConfig } from "../../config";
// hapi-auth-jwt2 expects the key to be a raw key
const jwkToHapiAuthJwt2 = (jwkString) => {
try {
const jwk = JSON.parse(jwkString);
return Buffer.from(jwk.k, "base64");
} catch {
throw new Error(
"Failed to parse key for JWT verification. This is probably an application configuration error."
);
}
};
const jwtDefaults = {
jwkeysB64: undefined,
validate: undefined,
strategyName: "nextauth-jwt",
};
const jwtRegister = async (server: Hapi.Server, options): Promise<void> => {
server.dependency(["hapi-auth-jwt2"]);
const settings = Hoek.applyToDefaults(jwtDefaults, options);
const key = settings.jwkeysB64.map((k) => jwkToHapiAuthJwt2(k));
if (!settings.strategyName) {
throw new Error("Missing strategy name in nextauth-jwt pluginsettings!");
}
server.auth.strategy(settings.strategyName, "jwt", {
key,
cookieKey: false,
urlKey: false,
validate: settings.validate,
});
};
export const registerNextAuthJwt = async (
server: Hapi.Server,
config: IAppConfig
): Promise<void> => {
if (config.nextAuth.signingKey) {
await server.register({
plugin: {
name: "nextauth-jwt",
version: "0.0.2",
register: jwtRegister,
},
options: {
jwkeysB64: [config.nextAuth.signingKey],
async validate(decoded, request: Hapi.Request) {
const { email, name, role } = decoded;
const user = await request.db().users.findBy({ email });
if (!config.isProd) {
server.logger.info(
{
email,
name,
role,
},
"nextauth-jwt authorizing request"
);
// server.logger.info({ user }, "nextauth-jwt user result");
}
return {
isValid: Boolean(user && user.isActive),
// this credentials object is made available in every request
// at `request.auth.credentials`
credentials: { email, name, role },
};
},
},
});
} else if (config.isProd) {
throw new Error("Missing nextauth.signingKey configuration value.");
} else {
server.log(
["warn"],
"Missing nextauth.signingKey configuration value. Authentication of nextauth endpoints disabled!"
);
}
};
// @hapi/jwt expects the key in its own format
/* UNUSED
const _jwkToHapiJwt = (jwkString) => {
try {
const jwk = JSON.parse(jwkString);
const rawKey = Buffer.from(jwk.k, "base64");
return {
key: rawKey,
algorithms: [jwk.alg],
kid: jwk.kid,
};
} catch {
throw new Error(
"Failed to parse key for JWT verification. This is probably an application configuration error."
);
}
};
*/

View file

@ -4,7 +4,7 @@ import Toys from "@hapipal/toys";
export const withDefaults = Toys.withRouteDefaults({
options: {
cors: true,
auth: "nextauth-jwt",
auth: "session-id-bearer-token",
validate: {
failAction: Metamigo.validatingFailAction,
},

View file

@ -1,9 +1,9 @@
import isFunction from "lodash/isFunction";
import isFunction from "lodash/isFunction.js";
import type * as Hapi from "@hapi/hapi";
import * as UserRoutes from "./users";
import * as VoiceRoutes from "./voice";
import * as WhatsappRoutes from "./whatsapp";
import * as SignalRoutes from "./signal";
import * as UserRoutes from "./users/index.js";
import * as VoiceRoutes from "./voice/index.js";
import * as WhatsappRoutes from "./whatsapp/index.js";
import * as SignalRoutes from "./signal/index.js";
const loadRouteIndex = async (server, index) => {
const routes = [];

View file

@ -1,6 +1,6 @@
import * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import * as Helpers from "../helpers";
import Joi from "joi";
import * as Helpers from "../helpers/index.js";
import Boom from "@hapi/boom";
const getSignalService = (request) => request.services("app").signaldService;

View file

@ -1,11 +1,11 @@
import * as Joi from "joi";
import Joi from "joi";
import * as Hapi from "@hapi/hapi";
import {
UserRecord,
crudRoutesFor,
CrudControllerBase,
} from "@digiresilience/metamigo-common";
import * as RouteHelpers from "../helpers";
import * as RouteHelpers from "../helpers/index.js";
class UserRecordController extends CrudControllerBase(UserRecord) {}

View file

@ -1,8 +1,8 @@
import * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import Joi from "joi";
import * as Boom from "@hapi/boom";
import * as R from "remeda";
import * as Helpers from "../helpers";
import * as Helpers from "../helpers/index.js";
import Twilio from "twilio";
import {
crudRoutesFor,
@ -66,7 +66,7 @@ export const VoiceProviderRoutes = Helpers.withDefaults([
},
]);
class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) { }
class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) {}
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
create: {
@ -122,4 +122,4 @@ export const VoiceLineRoutes = Helpers.withDefaults(
)
);
export * from "./twilio";
export * from "./twilio/index.js";

View file

@ -1,13 +1,13 @@
import * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import Joi from "joi";
import * as Boom from "@hapi/boom";
import Twilio from "twilio";
import { SavedVoiceProvider } from "@digiresilience/metamigo-db";
import pMemoize from "p-memoize";
import ExpiryMap from "expiry-map";
import ms from "ms";
import * as Helpers from "../../helpers";
import workerUtils from "../../../../worker-utils";
import * as Helpers from "../../helpers/index.js";
import workerUtils from "../../../../worker-utils.js";
const queueRecording = async (meta) =>
workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid });
@ -91,7 +91,7 @@ export const TwilioRoutes = Helpers.noAuth([
},
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
const { voiceLineId } = request.params;
const { To } = request.payload as { To: string; };
const { To } = request.payload as { To: string };
const voiceLine = await request.db().voiceLines.findBy({ number: To });
if (!voiceLine) return Boom.notFound();
if (voiceLine.id !== voiceLineId) return Boom.badRequest();
@ -193,7 +193,7 @@ export const TwilioRoutes = Helpers.noAuth([
},
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const { providerId } = request.params as { providerId: string; };
const { providerId } = request.params as { providerId: string };
const provider: SavedVoiceProvider = await request
.db()
.voiceProviders.findById({ id: providerId });

View file

@ -1,5 +1,5 @@
import * as Hapi from "@hapi/hapi";
import * as Helpers from "../helpers";
import * as Helpers from "../helpers/index.js";
import Boom from "@hapi/boom";
export const GetAllWhatsappBotsRoute = Helpers.withDefaults({

View file

@ -1,7 +1,7 @@
import type * as Hapi from "@hapi/hapi";
import SettingsService from "./settings";
import WhatsappService from "./whatsapp";
import SignaldService from "./signald";
import SettingsService from "./settings.js";
import WhatsappService from "./whatsapp.js";
import SignaldService from "./signald.js";
export const register = async (server: Hapi.Server): Promise<void> => {
// register your services here

View file

@ -8,7 +8,7 @@ import {
ClientMessageWrapperv1,
} from "@digiresilience/node-signald";
import { SavedSignalBot as Bot } from "@digiresilience/metamigo-db";
import workerUtils from "../../worker-utils";
import workerUtils from "../../worker-utils.js";
export default class SignaldService extends Service {
signald: SignaldAPI;

View file

@ -15,7 +15,7 @@ import makeWASocket, {
useMultiFileAuthState,
} from "@adiwajshing/baileys";
import fs from "fs";
import workerUtils from "../../worker-utils";
import workerUtils from "../../worker-utils.js";
export type AuthCompleteCallback = (error?: string) => void;

View file

@ -0,0 +1,2 @@
export * from "./server/index.js";
export * from "./logger.js";

View file

@ -0,0 +1,8 @@
import { startWithout } from "@digiresilience/montar";
import "./index.js";
async function runServer(): Promise<void> {
await startWithout(["worker"]);
}
runServer();

View file

@ -1,7 +1,7 @@
import * as Metamigo from "@digiresilience/metamigo-common";
import { defState } from "@digiresilience/montar";
import Manifest from "./manifest";
import config, { IAppConfig } from "../config";
import Manifest from "./manifest.js";
import config, { IAppConfig } from "../config.js";
export const deployment = async (
config: IAppConfig,

View file

@ -2,11 +2,8 @@ import * as Glue from "@hapi/glue";
import * as Metamigo from "@digiresilience/metamigo-common";
import * as Blipp from "blipp";
import HapiBasic from "@hapi/basic";
import HapiJwt from "hapi-auth-jwt2";
import HapiPostgraphile from "hapi-postgraphile";
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
import AppPlugin from "../app";
import type { IAppConfig } from "../config";
import AppPlugin from "../app/index.js";
import type { IAppConfig } from "../config.js";
const build = async (config: IAppConfig): Promise<Glue.Manifest> => {
const { port, address } = config.server;
@ -24,9 +21,6 @@ const build = async (config: IAppConfig): Promise<Glue.Manifest> => {
},
register: {
plugins: [
// jwt plugin, required for our jwt auth plugin
{ plugin: HapiJwt },
// Blipp prints the nicely formatted list of endpoints at app boot
{ plugin: Blipp },
@ -43,30 +37,6 @@ const build = async (config: IAppConfig): Promise<Glue.Manifest> => {
config,
},
},
// load Postgraphile
{
plugin: HapiPostgraphile,
options: {
route: {
path: "/graphql",
options: {
auth: {
strategies: ["nextauth-jwt"],
mode: "optional",
},
},
},
pgConfig: config.postgraphile.authConnection,
schemaName: "app_public",
schemaOptions: {
...getPostGraphileOptions(),
jwtAudiences: [config.nextAuth.audience],
jwtSecret: "",
// unauthenticated users will hit the database with this role
pgDefaultRole: "app_anonymous",
},
},
},
],
},
};

View file

@ -1,6 +1,6 @@
import * as Worker from "graphile-worker";
import { defState } from "@digiresilience/montar";
import config from "./config";
import config from "./config.js";
const startWorkerUtils = async (): Promise<Worker.WorkerUtils> => {
const workerUtils = await Worker.makeWorkerUtils({

View file

@ -5,8 +5,18 @@
"rootDir": "src",
"skipLibCheck": true,
"types": ["jest", "node", "long"],
"lib": ["es2020", "DOM"]
"lib": ["es2020", "DOM"],
"composite": true,
},
"include": ["src/**/*.ts", "src/**/.*.ts"],
"exclude": ["node_modules/**"]
"exclude": ["node_modules/**"],
"references": [
{"path": "../../packages/metamigo-common" },
{"path": "../../packages/metamigo-config" },
{"path": "../../packages/metamigo-db" },
{"path": "../../packages/hapi-nextauth" },
{"path": "../../packages/hapi-pg-promise" },
{"path": "../../packages/node-signald" },
{"path": "../../packages/montar" }
]
}

View file

@ -0,0 +1,12 @@
require("eslint-config-link/patch/modern-module-resolution");
module.exports = {
extends: [
"eslint-config-link/profile/node",
"eslint-config-link/profile/typescript",
"eslint-config-link/profile/jest",
],
parserOptions: { tsconfigRootDir: __dirname },
rules: {
"new-cap": "off"
},
};

View file

@ -0,0 +1,54 @@
FROM node:20 as base
FROM base AS builder
ARG APP_DIR=/opt/metamigo-cli
RUN mkdir -p ${APP_DIR}/
RUN npm i -g turbo
WORKDIR ${APP_DIR}
COPY . .
RUN turbo prune --scope=@digiresilience/metamigo-cli --docker
FROM base AS installer
ARG APP_DIR=/opt/metamigo-cli
WORKDIR ${APP_DIR}
COPY .gitignore .gitignore
COPY --from=builder ${APP_DIR}/out/json/ .
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
RUN npm ci --omit=dev
COPY --from=builder ${APP_DIR}/out/full/ .
RUN npm i -g turbo
RUN turbo run build --filter=metamigo-cli
FROM base AS runner
ARG APP_DIR=/opt/metamigo-cli
WORKDIR ${APP_DIR}/
ARG BUILD_DATE
ARG VERSION
LABEL maintainer="Darren Clarke <darren@redaranj.com>"
LABEL org.label-schema.build-date=$BUILD_DATE
LABEL org.label-schema.version=$VERSION
ENV APP_DIR ${APP_DIR}
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends \
dumb-init
RUN mkdir -p ${APP_DIR}
RUN chown -R node ${APP_DIR}/
USER node
WORKDIR ${APP_DIR}
COPY --from=installer ${APP_DIR}/node_modules/ ./node_modules/
COPY --from=installer ${APP_DIR}/packages/ ./packages/
COPY --from=installer ${APP_DIR}/apps/metamigo-cli/ ./apps/metamigo-cli/
COPY --from=installer ${APP_DIR}/apps/metamigo-api/ ./apps/metamigo-api/
COPY --from=installer ${APP_DIR}/apps/metamigo-worker/ ./apps/metamigo-worker/
COPY --from=installer ${APP_DIR}/package.json ./package.json
USER root
WORKDIR ${APP_DIR}/apps/metamigo-cli/
RUN chmod +x docker-entrypoint.sh
USER node
EXPOSE 3000
ENV PORT 3000
ENV NODE_ENV production
ENTRYPOINT ["/opt/metamigo-cli/apps/metamigo-cli/docker-entrypoint.sh"]

View file

@ -0,0 +1,3 @@
{
"presets": ["babel-preset-link"]
}

4
apps/metamigo-cli/cli Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
node ./build/main/index.js ${@}

View file

@ -1,8 +1,6 @@
#!/bin/bash
#!/usr/bin/env bash
set -e
cd ${AMIGO_DIR}
if [[ "$1" == "api" ]]; then
echo "docker-entrypoint: starting api server"
./cli db -- migrate
@ -10,9 +8,6 @@ if [[ "$1" == "api" ]]; then
elif [[ "$1" == "worker" ]]; then
echo "docker-entrypoint: starting worker"
exec dumb-init ./cli worker
elif [[ "$1" == "frontend" ]]; then
echo "docker-entrypoint: starting frontend"
exec dumb-init yarn workspace @app/frontend start
elif [[ "$1" == "cli" ]]; then
echo "docker-entrypoint: starting frontend"
shift 1

View file

@ -0,0 +1,4 @@
{
"preset": "jest-config-link",
"setupFiles": ["<rootDir>/src/setup.test.ts"]
}

View file

@ -0,0 +1,43 @@
{
"name": "@digiresilience/metamigo-cli",
"version": "0.2.0",
"main": "build/main/index.js",
"author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later",
"type": "module",
"bin": {
"metamigo": "./build/main/index.js"
},
"dependencies": {
"@digiresilience/montar": "*",
"@digiresilience/metamigo-config": "*",
"@digiresilience/metamigo-common": "*",
"@digiresilience/metamigo-db": "*",
"@digiresilience/metamigo-api": "*",
"@digiresilience/metamigo-worker": "*",
"commander": "^10.0.1",
"graphile-migrate": "^1.4.1",
"graphile-worker": "^0.13.0",
"node-jose": "^2.2.0",
"postgraphile": "4.12.3",
"graphql": "15.8.0"
},
"devDependencies": {
"@types/jest": "^29.5.1",
"pino-pretty": "^10.0.0",
"nodemon": "^2.0.22",
"tsconfig-link": "*",
"eslint-config-link": "*",
"jest-config-link": "*",
"babel-preset-link": "*",
"typescript": "^5.0.4"
},
"scripts": {
"migrate": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js db -- migrate",
"build": "tsc -p tsconfig.json",
"fix:lint": "eslint src --ext .ts --fix",
"fmt": "prettier \"src/**/*.ts\" --write",
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
"test": "echo no tests"
}
}

View file

@ -0,0 +1,22 @@
import {
generateConfig,
printConfigOptions,
} from "@digiresilience/metamigo-common";
import { IAppConfig, IAppConvict } from "@digiresilience/metamigo-config";
import { loadConfigRaw } from "@digiresilience/metamigo-config";
export const genConf = async (): Promise<void> => {
const c = (await loadConfigRaw()) as any;
const generated = generateConfig(c) as any;
console.log(generated);
};
export const genSchema = async (): Promise<void> => {
const c: any = await loadConfigRaw();
console.log(c.getSchemaString());
};
export const listConfig = async (): Promise<void> => {
const c = (await loadConfigRaw()) as any;
printConfigOptions(c);
};

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
import { Command } from "commander";
import { startWithout } from "@digiresilience/montar";
import { migrateWrapper } from "@digiresilience/metamigo-db";
import { loadConfig } from "@digiresilience/metamigo-config";
import { genConf, listConfig } from "./config.js";
import { createTokenForTesting, generateJwks } from "./jwks.js";
import { exportGraphqlSchema } from "./metamigo-postgraphile.js";
import "@digiresilience/metamigo-api";
import "@digiresilience/metamigo-worker";
const program = new Command();
export async function runServer(): Promise<void> {
await startWithout(["worker"]);
}
export async function runWorker(): Promise<void> {
await startWithout(["server"]);
}
program
.command("config-generate")
.description("Generate a sample JSON configuration file (to stdout)")
.action(genConf);
program
.command("config-help")
.description("Prints the entire convict config ")
.action(listConfig);
program
.command("api")
.description("Run the application api server")
.action(runServer);
program
.command("worker")
.description("Run the worker to process jobs")
.action(runWorker);
program
.command("db <commands...>")
.description("Run graphile-migrate commands with your app's config loaded.")
.action(async (args) => {
const config = await loadConfig();
return migrateWrapper(args, config);
});
program
.command("gen-jwks")
.description("Generate the JWKS")
.action(generateJwks);
program
.command("gen-testing-jwt")
.description("Generate a JWT for the test suite")
.action(createTokenForTesting);
program
.command("export-graphql-schema")
.description("Export the graphql schema")
.action(exportGraphqlSchema);
program.parse(process.argv);

View file

@ -0,0 +1,67 @@
import jose from "node-jose";
import * as jwt from "jsonwebtoken";
const generateKeystore = async () => {
const keystore = jose.JWK.createKeyStore();
await keystore.generate("oct", 256, {
alg: "A256GCM",
use: "enc",
});
await keystore.generate("oct", 256, {
alg: "HS512",
use: "sig",
});
return keystore;
};
const safeString = (input) =>
Buffer.from(JSON.stringify(input)).toString("base64");
const stringify = (v) => JSON.stringify(v, undefined, 2);
const _generateJwks = async () => {
const keystore = await generateKeystore();
const encryption = keystore.all({ use: "enc" })[0].toJSON(true);
const signing = keystore.all({ use: "sig" })[0].toJSON(true);
return {
nextAuth: {
signingKeyB64: safeString(signing),
encryptionKeyB64: safeString(encryption),
},
};
};
export const generateJwks = async (): Promise<void> => {
console.log(stringify(await _generateJwks()));
};
export const createTokenForTesting = async (): Promise<void> => {
const keys = await _generateJwks();
const signingKey = Buffer.from(
JSON.parse(
Buffer.from(keys.nextAuth.signingKeyB64, "base64").toString("utf-8")
).k,
"base64"
);
const token = jwt.sign(
{
iss: "Test Env",
iat: 1606893960,
aud: "metamigo",
sub: "abel@guardianproject.info",
name: "Abel Luck",
email: "abel@guardianproject.info",
userRole: "admin",
},
signingKey,
{ expiresIn: "100y", algorithm: "HS512" }
);
console.log("CONFIG");
console.log(stringify(keys));
console.log();
console.log("TOKEN");
console.log(token);
console.log();
};

View file

@ -0,0 +1,40 @@
import { writeFileSync } from "node:fs";
import {
getIntrospectionQuery,
GraphQLSchema,
graphqlSync,
lexicographicSortSchema,
printSchema,
} from "graphql";
import { createPostGraphileSchema } from "postgraphile";
import pg from "pg";
import { loadConfig } from "@digiresilience/metamigo-config";
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
const { Pool } = pg;
export const exportGraphqlSchema = async (): Promise<void> => {
const config = await loadConfig();
const rootPgPool = new Pool({
connectionString: config.db.connection,
});
const exportSchema = `../../data/schema.graphql`;
const exportJson = `../../frontend/lib/graphql-schema.json`;
try {
const schema = (await createPostGraphileSchema(
config.postgraphile.authConnection,
"app_public",
getPostGraphileOptions()
)) as unknown as GraphQLSchema;
const sorted = lexicographicSortSchema(schema);
const json = graphqlSync({ schema, source: getIntrospectionQuery() });
writeFileSync(exportSchema, printSchema(sorted));
writeFileSync(exportJson, JSON.stringify(json));
console.log(`GraphQL schema exported to ${exportSchema}`);
console.log(`GraphQL schema json exported to ${exportJson}`);
} finally {
rootPgPool.end();
}
};

View file

@ -0,0 +1,14 @@
{
"extends": "tsconfig-link",
"compilerOptions": {
"incremental": true,
"outDir": "build/main",
"rootDir": "src",
"baseUrl": "./",
"skipLibCheck": true,
"types": ["jest", "node"],
"esModuleInterop": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"]
}

View file

@ -1,61 +1,52 @@
FROM node:20-bullseye as builder
FROM node:20 as base
ARG METAMIGO_DIR=/opt/metamigo
RUN mkdir -p ${METAMIGO_DIR}/
WORKDIR ${METAMIGO_DIR}
COPY package.json tsconfig.json ${METAMIGO_DIR}/
COPY . ${METAMIGO_DIR}/
FROM base AS builder
ARG APP_DIR=/opt/metamigo-frontend
RUN mkdir -p ${APP_DIR}/
RUN npm i -g turbo
WORKDIR ${APP_DIR}
COPY . .
RUN turbo prune --scope=@digiresilience/metamigo-frontend --docker
RUN npm --no-install tsc --build --verbose
RUN npm install
RUN npm run build
RUN rm -Rf ./node_modules
FROM base AS installer
ARG APP_DIR=/opt/metamigo-frontend
WORKDIR ${APP_DIR}
COPY .gitignore .gitignore
COPY --from=builder ${APP_DIR}/out/json/ .
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
RUN npm ci --omit=dev
FROM node:20-bullseye as clean
ARG METAMIGO_DIR=/opt/metamigo
COPY --from=builder ${METAMIGO_DIR} ${METAMIGO_DIR}/
RUN rm -Rf ./node_modules
FROM node:20-bullseye as pristine
LABEL maintainer="Abel Luck <abel@guardianproject.info>"
COPY --from=builder ${APP_DIR}/out/full/ .
RUN npm i -g turbo
RUN turbo run build --filter=metamigo-frontend
FROM base AS runner
ARG APP_DIR=/opt/metamigo-frontend
WORKDIR ${APP_DIR}/
ARG BUILD_DATE
ARG VERSION
LABEL maintainer="Darren Clarke <darren@redaranj.com>"
LABEL org.label-schema.build-date=$BUILD_DATE
LABEL org.label-schema.version=$VERSION
ENV APP_DIR ${APP_DIR}
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends --fix-missing \
postgresql-client dumb-init ffmpeg
ARG METAMIGO_DIR=/opt/metamigo
ENV METAMIGO_DIR ${METAMIGO_DIR}
RUN mkdir -p ${METAMIGO_DIR}
RUN chown -R node:node ${METAMIGO_DIR}/
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
COPY --from=clean ${METAMIGO_DIR}/ ${METAMIGO_DIR}/
WORKDIR ${METAMIGO_DIR}
apt-get install -y --no-install-recommends \
dumb-init
RUN mkdir -p ${APP_DIR}
RUN chown -R node ${APP_DIR}/
USER node
WORKDIR ${APP_DIR}
COPY --from=installer ${APP_DIR}/node_modules/ ./node_modules/
COPY --from=installer ${APP_DIR}/packages/ ./packages/
COPY --from=installer ${APP_DIR}/apps/metamigo-frontend/ ./apps/metamigo-frontend/
COPY --from=installer ${APP_DIR}/package.json ./package.json
USER root
WORKDIR ${APP_DIR}/apps/metamigo-frontend/
RUN chmod +x docker-entrypoint.sh
USER node
EXPOSE 3000
EXPOSE 3001
EXPOSE 3002
ENV PORT 3000
ENV NODE_ENV production
ARG BUILD_DATE
ARG VCS_REF
ARG VCS_URL="https://gitlab.com/digiresilience/link/metamigo"
ARG VERSION
LABEL org.label-schema.schema-version="1.0"
LABEL org.label-schema.name="digiresilience.org/link/metamigo"
LABEL org.label-schema.description="part of CDR Link"
LABEL org.label-schema.build-date=$BUILD_DATE
LABEL org.label-schema.vcs-url=$VCS_URL
LABEL org.label-schema.vcs-ref=$VCS_REF
LABEL org.label-schema.version=$VERSION
ENTRYPOINT ["/docker-entrypoint.sh"]
ENTRYPOINT ["/opt/metamigo-frontend/apps/metamigo-frontend/docker-entrypoint.sh"]

View file

@ -1,3 +1,3 @@
export {default as AppBar} from "./AppBar";
export {default as Layout} from "./Layout";
export {default as Menu} from "./Menu";
export { default as AppBar } from "./AppBar";
export { default as Layout } from "./Layout";
export { default as Menu } from "./Menu";

View file

@ -13,7 +13,9 @@ export const theme = {
background: {
default: "#fff",
},
getContrastText(color: string) { return color === "#ffffff" ? "#000" : "#fff"; },
getContrastText(color: string) {
return color === "#ffffff" ? "#000" : "#fff";
},
},
shape: {
borderRadius: 5,

View file

@ -138,7 +138,7 @@ const handleRequestCode = async ({
verifyMode,
id,
onSuccess,
onFailure,
onError,
captchaCode = undefined,
}: any) => {
if (verifyMode === MODE.SMS) console.log("REQUESTING sms");
@ -160,7 +160,7 @@ const handleRequestCode = async ({
if (response && response.ok) {
onSuccess();
} else {
onFailure(response.status || 400);
onError(response.status || 400);
}
} catch (error: any) {
console.error("Failed to request verification code:", error);
@ -171,7 +171,7 @@ const VerificationCodeRequest = ({
verifyMode,
data,
onSuccess,
onFailure,
onError,
}: any) => {
React.useEffect(() => {
(async () => {
@ -179,10 +179,10 @@ const VerificationCodeRequest = ({
verifyMode,
id: data.id,
onSuccess,
onFailure,
onError,
});
})();
}, [data.id, onFailure, onSuccess, verifyMode]);
}, [data.id, onError, onSuccess, verifyMode]);
return (
<>
@ -204,7 +204,7 @@ const VerificationCaptcha = ({
verifyMode,
data,
onSuccess,
onFailure,
onError,
handleClose,
}: any) => {
const [code, setCode] = React.useState(undefined);
@ -216,7 +216,7 @@ const VerificationCaptcha = ({
verifyMode,
id: data.id,
onSuccess,
onFailure,
onError,
captchaCode: code,
});
setSubmitting(false);
@ -367,7 +367,7 @@ const VerificationCodeDialog = (props: any) => {
props.handleClose();
};
const onFailure = (code: number) => {
const onError = (code: number) => {
if (code === 402 || code === 500) {
setStage("captcha");
} else {
@ -385,7 +385,7 @@ const VerificationCodeDialog = (props: any) => {
<VerificationCodeRequest
mode={props.verifyMode}
onSuccess={onRequestSuccess}
onFailure={onFailure}
onError={onError}
{...props}
/>
)}
@ -400,7 +400,7 @@ const VerificationCodeDialog = (props: any) => {
<VerificationCaptcha
mode={props.verifyMode}
onSuccess={onRequestSuccess}
onFailure={onRestartVerification}
onError={onRestartVerification}
handleClose={handleClose}
{...props}
/>

View file

@ -9,10 +9,10 @@ import {
import { useSession } from "next-auth/react";
import { UserRoleInput } from "./shared";
const UserCreate: FC<CreateProps> = (props: any) => {
const UserCreate: FC<CreateProps> = () => {
const { data: session } = useSession();
return (
<Create {...props} title="Create Users">
<Create title="Create Users">
<SimpleForm>
<TextInput source="email" />
<TextInput source="name" />

View file

@ -8,8 +8,8 @@ import {
Toolbar,
SaveButton,
DeleteButton,
EditProps,
useRedirect,
useRecordContext,
} from "react-admin";
import { useSession } from "next-auth/react";
import { UserRoleInput } from "./shared";
@ -23,16 +23,20 @@ const useStyles = makeStyles((_theme: any) => ({
}));
const UserEditToolbar = (props: any) => {
const classes = useStyles(props);
const classes = useStyles();
const redirect = useRedirect();
const record = useRecordContext();
const {session} = props;
const shouldDisableDelete = !session || !session.user || session.user.id === record.id;
return (
<Toolbar className={classes.defaultToolbar} {...props}>
<Toolbar className={classes.defaultToolbar}>
<SaveButton
label="save"
mutationOptions={{ onSuccess: () => redirect("/users") }}
/>
<DeleteButton disabled={props.session.user.id === props.record.id} />
<DeleteButton disabled={shouldDisableDelete} />
</Toolbar>
);
};
@ -43,11 +47,11 @@ const UserTitle = ({ record }: { record?: any }) => {
return <span>User {title}</span>;
};
const UserEdit = (props: EditProps) => {
const UserEdit = () => {
const { data: session } = useSession();
return (
<Edit title={<UserTitle />} {...props}>
<Edit title={<UserTitle />}>
<SimpleForm toolbar={<UserEditToolbar session={session} />}>
<TextInput disabled source="id" />
<TextInput source="email" />

View file

@ -6,11 +6,10 @@ import {
TextField,
EmailField,
BooleanField,
ListProps,
} from "react-admin";
const UserList = (props: ListProps) => (
<List {...props} exporter={false}>
const UserList = () => (
<List exporter={false}>
<Datagrid rowClick="edit">
<EmailField source="email" />
<DateField source="emailVerified" />

View file

@ -1,14 +1,17 @@
import { SelectInput } from "react-admin";
import { SelectInput, useRecordContext } from "react-admin";
export const UserRoleInput = (props: any) => (
<SelectInput
source="userRole"
choices={[
{ id: "NONE", name: "None" },
{ id: "USER", name: "User" },
{ id: "ADMIN", name: "Admin" },
]}
disabled={props.session.user.id === props.record.id}
{...props}
/>
);
export const UserRoleInput = (props: any) => {
const record = useRecordContext();
return (
<SelectInput
source="userRole"
choices={[
{ id: "NONE", name: "None" },
{ id: "USER", name: "User" },
{ id: "ADMIN", name: "Admin" },
]}
disabled={props.session.user.id === record.id}
{...props}
/>
);
};

View file

@ -117,7 +117,7 @@ const Sidebar = ({ record }: any) => {
const WhatsappBotShow = (props: ShowProps) => {
const refresh = useRefresh();
const { data } = useGetOne("whatsappBots", props.id as any);
const { data } = useGetOne("whatsappBots", {id: props.id});
const { data: registerData, error: registerError } = useSWR(
data && !data?.isVerified

View file

@ -1,23 +1,5 @@
#!/bin/bash
set -e
cd ${AMIGO_DIR}
if [[ "$1" == "api" ]]; then
echo "docker-entrypoint: starting api server"
./cli db -- migrate
exec dumb-init ./cli api
elif [[ "$1" == "worker" ]]; then
echo "docker-entrypoint: starting worker"
exec dumb-init ./cli worker
elif [[ "$1" == "frontend" ]]; then
echo "docker-entrypoint: starting frontend"
exec dumb-init yarn workspace @app/frontend start
elif [[ "$1" == "cli" ]]; then
echo "docker-entrypoint: starting frontend"
shift 1
exec ./cli "$@"
else
echo "docker-entrypoint: missing argument, one of: api, worker, frontend, cli"
exit 1
fi
echo "starting leafcutter"
exec dumb-init npm run start

View file

@ -25,8 +25,7 @@ const customEnglishMessages: TranslationMessages = {
signalBots: {
name: "Signal Bot |||| Signal Bots",
verifyDialog: {
sms:
"Please enter the verification code sent via SMS to %{phoneNumber}",
sms: "Please enter the verification code sent via SMS to %{phoneNumber}",
voice:
"Please answer the call from Signal to %{phoneNumber} and enter the verification code",
},

View file

@ -100,13 +100,15 @@ export const getIdentity = async (
const cloudflareAccountProvider = "cloudflare-access";
const cloudflareAuthorizeCallback = (
req: IncomingMessage,
domain: string,
verifier: VerifyFn,
adapter: Adapter
): (() => Promise<any>) => async () => {
/*
const cloudflareAuthorizeCallback =
(
req: IncomingMessage,
domain: string,
verifier: VerifyFn,
adapter: Adapter
): (() => Promise<any>) =>
async () => {
/*
lots of little variables in here.
@ -118,75 +120,75 @@ const cloudflareAuthorizeCallback = (
profile: this is the accumulated user information we have that we will fetch/build the user record with
*/
const { token, decoded } = await verifyRequest(verifier, req);
const { token, decoded } = await verifyRequest(verifier, req);
const profile = {
email: undefined,
name: undefined,
avatar: undefined,
const profile = {
email: undefined,
name: undefined,
avatar: undefined,
};
if (decoded.email) profile.email = decoded.email;
if (decoded.name) profile.name = decoded.name;
const identity = await getIdentity(domain, token);
if (identity.email) profile.email = identity.email;
if (identity.name) profile.name = identity.name;
if (!profile.email)
throw new Error("cloudflare access authorization: email not found");
const providerId = `cfaccess|${identity.idp.type}|${identity.idp.id}`;
const providerAccountId = identity.user_uuid;
if (!providerAccountId)
throw new Error(
"cloudflare access authorization: missing provider account id"
);
const {
getUserByProviderAccountId,
getUserByEmail,
createUser,
linkAccount,
} =
// @ts-expect-error: non-existent property
await adapter.getAdapter({} as any);
const userByProviderAccountId = await getUserByProviderAccountId(
providerId,
providerAccountId
);
if (userByProviderAccountId) {
return userByProviderAccountId;
}
const userByEmail = await getUserByEmail(profile.email);
if (userByEmail) {
// we will not explicitly link accounts
throw new Error(
"cloudflare access authorization: user exists for email address, but is not linked."
);
}
const user = await createUser(profile);
// between the previous line and the next line exists a transactional bug
// https://github.com/nextauthjs/next-auth/issues/876
// hopefully we don't experience it
await linkAccount(
user.id,
providerId,
cloudflareAccountProvider,
providerAccountId,
// the following are unused but are specified for completness
undefined,
undefined,
undefined
);
return user;
};
if (decoded.email) profile.email = decoded.email;
if (decoded.name) profile.name = decoded.name;
const identity = await getIdentity(domain, token);
if (identity.email) profile.email = identity.email;
if (identity.name) profile.name = identity.name;
if (!profile.email)
throw new Error("cloudflare access authorization: email not found");
const providerId = `cfaccess|${identity.idp.type}|${identity.idp.id}`;
const providerAccountId = identity.user_uuid;
if (!providerAccountId)
throw new Error(
"cloudflare access authorization: missing provider account id"
);
const {
getUserByProviderAccountId,
getUserByEmail,
createUser,
linkAccount,
} =
// @ts-expect-error: non-existent property
await adapter.getAdapter({} as any);
const userByProviderAccountId = await getUserByProviderAccountId(
providerId,
providerAccountId
);
if (userByProviderAccountId) {
return userByProviderAccountId;
}
const userByEmail = await getUserByEmail(profile.email);
if (userByEmail) {
// we will not explicitly link accounts
throw new Error(
"cloudflare access authorization: user exists for email address, but is not linked."
);
}
const user = await createUser(profile);
// between the previous line and the next line exists a transactional bug
// https://github.com/nextauthjs/next-auth/issues/876
// hopefully we don't experience it
await linkAccount(
user.id,
providerId,
cloudflareAccountProvider,
providerAccountId,
// the following are unused but are specified for completness
undefined,
undefined,
undefined
);
return user;
};
/**
* @param audience the cloudflare access audience id

View file

@ -8,8 +8,5 @@ export const metamigoDataProvider = async (client: any) => {
{},
{ introspection: { schema: schema.data.__schema } }
);
const dataProvider = async (type: any, resource: any, params: any) => graphqlDataProvider(type, resource, params);
return dataProvider;
return graphqlDataProvider;
};

View file

@ -1,8 +1,10 @@
/* eslint-disable unicorn/no-null */
/* eslint-disable max-params */
import type { Adapter } from "next-auth/adapters";
// @ts-expect-error: Missing export
import type { AppOptions } from "next-auth";
import type {
Adapter,
AdapterAccount,
AdapterSession,
AdapterUser,
} from "next-auth/adapters";
import * as Wreck from "@hapi/wreck";
import * as Boom from "@hapi/boom";
@ -18,7 +20,7 @@ export interface Profile {
createdBy: string;
}
export type User = Profile & { id: string; createdAt: Date; updatedAt: Date; };
export type User = Profile & { id: string; createdAt: Date; updatedAt: Date };
export interface Session {
userId: string;
@ -70,7 +72,7 @@ export const MetamigoAdapter = (config: IAppConfig): Adapter => {
json: "force",
});
async function getAdapter(_appOptions: AppOptions) {
function getAdapter(): Adapter {
async function createUser(profile: Profile) {
try {
if (!profile.createdBy) profile = { ...profile, createdBy: "nextauth" };
@ -106,19 +108,23 @@ export const MetamigoAdapter = (config: IAppConfig): Adapter => {
}
}
async function getUserByProviderAccountId(
providerId: string,
providerAccountId: string
) {
async function getUserByAccount({
providerAccountId,
provider,
}: {
providerAccountId: string;
provider: string;
}) {
try {
const { payload } = await wreck.get(
`getUserByProviderAccountId/${providerId}/${providerAccountId}`
`getUserByAccount/${provider}/${providerAccountId}`
);
return payload;
} catch (error) {
if (Boom.isBoom(error, 404)) return null;
throw new Error("GET_USER_BY_PROVIDER_ACCOUNT_ID");
console.log(error);
throw new Error("GET_USER_BY_ACCOUNT");
}
}
@ -134,52 +140,46 @@ export const MetamigoAdapter = (config: IAppConfig): Adapter => {
}
}
async function linkAccount(
userId: string,
providerId: string,
providerType: string,
providerAccountId: string,
refreshToken: string,
accessToken: string,
accessTokenExpires: number
) {
async function linkAccount(account: AdapterAccount) {
try {
const payload = {
userId,
providerId,
providerType,
providerAccountId: `${providerAccountId}`, // must be a string
refreshToken,
accessToken,
accessTokenExpires,
};
await wreck.put("linkAccount", {
payload,
});
} catch {
await wreck.put("linkAccount", { payload: account } as any);
} catch (error) {
console.log(error);
throw new Error("LINK_ACCOUNT_ERROR");
}
}
async function createSession(user: User) {
try {
const { payload } = await wreck.post("createSession", {
payload: user,
});
const { payload }: { payload: AdapterSession } = await wreck.post(
"createSession",
{
payload: user,
}
);
payload.expires = new Date(payload.expires);
return payload;
} catch {
} catch (error) {
console.log(error);
throw new Error("CREATE_SESSION_ERROR");
}
}
async function getSession(sessionToken: string) {
async function getSessionAndUser(sessionToken: string) {
try {
const { payload } = await wreck.get(`getSession/${sessionToken}`);
return payload;
const { payload }: { payload: any } = await wreck.get(
`getSessionAndUser/${sessionToken}`
);
const {
session,
user,
}: { session: AdapterSession; user: AdapterUser } = payload;
session.expires = new Date(session.expires);
return { session, user };
} catch (error) {
console.log(error);
if (Boom.isBoom(error, 404)) return null;
throw new Error("GET_SESSION_ERROR");
throw new Error("GET_SESSION_AND_USER_ERROR");
}
}
@ -213,21 +213,18 @@ export const MetamigoAdapter = (config: IAppConfig): Adapter => {
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
getUserByAccount,
updateUser,
// deleteUser,
linkAccount,
// unlinkAccount,
createSession,
getSession,
getSessionAndUser,
updateSession,
deleteSession,
// @ts-expect-error: Type error
} as AdapterInstance<Profile, User, Session, unknown>;
}
return {
// @ts-expect-error: non-existent property
getAdapter,
};
return getAdapter();
};

View file

@ -4,7 +4,8 @@ export const E164Regex = /^\+[1-9]\d{1,14}$/;
/**
* Returns true if the number is a valid E164 number
*/
export const isValidE164Number = (phoneNumber: string) => E164Regex.test(phoneNumber);
export const isValidE164Number = (phoneNumber: string) =>
E164Regex.test(phoneNumber);
/**
* Given a phone number approximation, will clean out whitespace and punctuation.

View file

@ -1,5 +1,5 @@
{
"name": "metamigo-frontend",
"name": "@digiresilience/metamigo-frontend",
"version": "0.2.0",
"private": true,
"dependencies": {
@ -38,7 +38,7 @@
"test": "echo no tests",
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.graphql && next lint && prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,graphql,md}\" --write",
"fix:lint": "eslint --ext .js,.jsx,.ts,.tsx,.graphql --fix",
"fmt": "prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,graphql,md}\" --list-different"
"fmt": "prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,graphql,md}\" --write"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.4.4",

View file

@ -60,38 +60,20 @@ const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => {
return {
secret: nextAuth.secret,
session: {
jwt: true,
strategy: "database",
maxAge: 8 * 60 * 60, // 8 hours
},
jwt: {
secret: nextAuth.secret,
encryption: false,
signingKey: nextAuth.signingKey,
encryptionKey: nextAuth.encryptionKey,
},
providers,
adapter,
callbacks: {
async session(session: any, token: any) {
// make the user id available in the react client
session.user.id = token.userId;
async session({ session, user }: any) {
session.user.id = user.id;
session.user.userRole = user.userRole;
return session;
},
async jwt(token: any, user: any) {
const isSignIn = Boolean(user);
// Add auth_time to token on signin in
if (isSignIn) {
// not sure what this does
// if (!token.aud) token.aud;
token.aud = nextAuth.audience;
token.picture = user.avatar;
token.userId = user.id;
token.role = user.userRole ? `app_${user.userRole}` : "app_anonymous";
}
return token;
},
},
};
};

View file

@ -4,7 +4,7 @@ export default createProxyMiddleware({
target:
process.env.NODE_ENV === "production"
? "http://metamigo-api:3001"
: "http://localhost:3001",
: "http://127.0.0.1:3001",
changeOrigin: true,
pathRewrite: { "^/graphql": "/graphql" },
xfwd: true,
@ -20,8 +20,6 @@ export default createProxyMiddleware({
let token = req.cookies["__Secure-next-auth.session-token"];
if (!token) token = req.cookies["next-auth.session-token"];
// console.log(req.body);
// if (req.body.query) console.log(req.body.query);
if (token) {
proxyReq.setHeader("authorization", `Bearer ${token}`);
proxyReq.removeHeader("cookie");

View file

@ -1,61 +0,0 @@
FROM node:20-bullseye as builder
ARG METAMIGO_DIR=/opt/metamigo
RUN mkdir -p ${METAMIGO_DIR}/
WORKDIR ${METAMIGO_DIR}
COPY package.json tsconfig.json ${METAMIGO_DIR}/
COPY . ${METAMIGO_DIR}/
RUN npx --no-install tsc --build --verbose
RUN npm install
RUN npm run build
RUN rm -Rf ./node_modules
FROM node:20-bullseye as clean
ARG METAMIGO_DIR=/opt/metamigo
COPY --from=builder ${METAMIGO_DIR} ${METAMIGO_DIR}/
RUN rm -Rf ./node_modules
FROM node:20-bullseye as pristine
LABEL maintainer="Abel Luck <abel@guardianproject.info>"
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends --fix-missing \
postgresql-client dumb-init ffmpeg
ARG METAMIGO_DIR=/opt/metamigo
ENV METAMIGO_DIR ${METAMIGO_DIR}
RUN mkdir -p ${METAMIGO_DIR}
RUN chown -R node:node ${METAMIGO_DIR}/
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
COPY --from=clean ${METAMIGO_DIR}/ ${METAMIGO_DIR}/
WORKDIR ${METAMIGO_DIR}
USER node
EXPOSE 3000
EXPOSE 3001
EXPOSE 3002
ENV PORT 3000
ENV NODE_ENV production
ARG BUILD_DATE
ARG VCS_REF
ARG VCS_URL="https://gitlab.com/digiresilience/link/metamigo"
ARG VERSION
LABEL org.label-schema.schema-version="1.0"
LABEL org.label-schema.name="digiresilience.org/link/metamigo"
LABEL org.label-schema.description="part of CDR Link"
LABEL org.label-schema.build-date=$BUILD_DATE
LABEL org.label-schema.vcs-url=$VCS_URL
LABEL org.label-schema.vcs-ref=$VCS_REF
LABEL org.label-schema.version=$VERSION
ENTRYPOINT ["/docker-entrypoint.sh"]

View file

@ -2,10 +2,10 @@ import * as Worker from "graphile-worker";
import { parseCronItems } from "graphile-worker";
import { defState } from "@digiresilience/montar";
import config from "@digiresilience/metamigo-config";
import { initPgp } from "./db";
import logger from "./logger";
import workerUtils from "./utils";
import { assertFfmpegAvailable } from "./lib/media-convert";
import { initPgp } from "./db.js";
import logger from "./logger.js";
import workerUtils from "./utils.js";
import { assertFfmpegAvailable } from "./lib/media-convert.js";
const logFactory = (scope: any) => (level: any, message: any, meta: any) => {
const pinoLevel = level === "warning" ? "warn" : level;

View file

@ -1,7 +1,8 @@
{
"name": "metamigo-worker",
"name": "@digiresilience/metamigo-worker",
"version": "0.2.0",
"main": "build/main/index.js",
"type": "module",
"author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later",
"dependencies": {
@ -44,14 +45,12 @@
"doc": "yarn run doc:html",
"fix:lint": "eslint src --ext .ts --fix",
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"worker": "NODE_ENV=development yarn cli worker",
"test:jest": "JEST_CIRCUS=1 jest --coverage --forceExit --detectOpenHandles --reporters=default --reporters=jest-junit",
"test:jest-verbose": "yarn test:jest --verbose --silent=false",
"test": "yarn test:jest",
"lint": "yarn lint:lint && yarn lint:prettier",
"lint:lint": "eslint src --ext .ts",
"lint:prettier": "prettier \"src/**/*.ts\" --list-different",
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "yarn test:jest --watchAll"
}
}