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

@ -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" }
]
}