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

5
.dockerignore Normal file
View file

@ -0,0 +1,5 @@
node_modules
out
signald
docker-compose.yml
README.md

18
.editorconfig Normal file
View file

@ -0,0 +1,18 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
[{Makefile,**.mk}]
# Use tabs for indentation (Makefiles require tabs)
indent_style = tab

4
.gitignore vendored
View file

@ -5,6 +5,7 @@ build/**
**/dist/** **/dist/**
.next/** .next/**
docker/zammad/addons/** docker/zammad/addons/**
!docker/zammad/addons/.gitkeep
.npmrc .npmrc
coverage/ coverage/
build/ build/
@ -20,3 +21,6 @@ coverage
.pgpass .pgpass
**/dist/** **/dist/**
.metamigo.local.json .metamigo.local.json
out/
signald-state/*
!./signald-state/.gitkeep

78
.gitpod.dockerfile Normal file
View file

@ -0,0 +1,78 @@
FROM gitpod/workspace-full
# install tools we need
RUN set -ex; \
pyenv global system; \
sudo add-apt-repository ppa:ansible/ansible; \
sudo add-apt-repository ppa:maxmind/ppa; \
curl -s https://helm.baltorepo.com/organization/signing.asc | sudo apt-key add - ; \
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash; \
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list ; \
sudo apt-get update; \
sudo apt-get -y upgrade ; \
sudo apt-get install -y \
ansible \
build-essential \
httpie \
fd-find \
ffmpeg \
geoipupdate \
gitlab-runner \
helm \
htop \
iotop \
iptraf \
jq \
kitty-terminfo \
libolm-dev \
ncdu \
postgresql \
pwgen \
python3-wheel \
ripgrep \
rsync \
scdaemon \
socat \
tmux \
unrar \
unzip \
vifm \
vim \
yamllint \
zsh \
zsh-syntax-highlighting \
; sudo rm -rf /var/lib/apt/lists/*
RUN set -ex; \
brew install \
zoxide \
fzf;
# needed for tailscale
RUN sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
# install npm global packages we need
RUN set -ex; \
npm install -g \
standard-version \
turbo \
;
# make a place for all our warez
RUN sudo mkdir -p /usr/local/bin
# install AWS' kubectl
# from https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html
ARG KUBECTL_URL="https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl"
RUN set -ex; \
curl -o kubectl "${KUBECTL_URL}"; \
chmod +x kubectl; \
sudo mv kubectl /usr/local/bin
# install cloudflared
# from https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION="2023.2.1"
RUN set -ex; \
wget --progress=dot:mega https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb; \
sudo dpkg -i cloudflared-linux-amd64.deb; \
cloudflared --version

63
.gitpod.yml Normal file
View file

@ -0,0 +1,63 @@
---
# build the docker image for our gitpod from this dockerfile
image:
file: .gitpod.dockerfile
# all init+before are run in prebuilds, and on workspace startup
tasks:
- name: npm install
init: |
npm install
# extra extensions we share
vscode:
extensions:
- redhat.vscode-yaml
- ms-azuretools.vscode-docker
- ms-kubernetes-tools.vscode-kubernetes-tools
- ms-vscode.makefile-tools
- bungcip.better-toml
- sleistner.vscode-fileutils
- esbenp.prettier-vscode
- darkriszty.markdown-table-prettify
- VisualStudioExptTeam.vscodeintellicode
ports:
- name: Zammad
port: 8001
onOpen: notify
- name: Leafcutter Local
port: 3001
onOpen: notify
- name: Leafcutter
port: 8004
onOpen: notify
- name: Link
port: 8003
onOpen: notify
- name: Link Local
port: 3000
onOpen: notify
- name: Metamigo
port: 8002
onOpen: notify
- name: Metamigo Local
port: 2999
onOpen: notify
- name: Metamigo API
port: 8004
onOpen: notify
- name: Zammad Postgres
port: 5432
onOpen: notify
- name: Metamigo Postgres
port: 5433
onOpen: notify

2
.nvmrc
View file

@ -1 +1 @@
v20 v20.2.0

View file

@ -58,13 +58,17 @@ setup-signal:
create-admin-user: create-admin-user:
docker exec -i $(shell docker ps -aqf "name=metamigo-postgresql") bash < ./scripts/create-admin-user.sh docker exec -i $(shell docker ps -aqf "name=metamigo-postgresql") bash < ./scripts/create-admin-user.sh
start:
.env:
@test -f .env || echo "You must create .env please refer to the README" && exit 1
start: .env
CURRENT_UID=$(CURRENT_UID) docker-compose up -d CURRENT_UID=$(CURRENT_UID) docker-compose up -d
start-dev: start-dev: .env
CURRENT_UID=$(CURRENT_UID) docker-compose up --build -d CURRENT_UID=$(CURRENT_UID) docker-compose up --build -d
restart: restart: .env
CURRENT_UID=$(CURRENT_UID) docker restart $(shell docker ps -a -q) CURRENT_UID=$(CURRENT_UID) docker restart $(shell docker ps -a -q)
stop: stop:
@ -73,3 +77,7 @@ stop:
destroy: destroy:
docker-compose down docker-compose down
docker volume prune docker volume prune
dev-metamigo:
CURRENT_UID=$(CURRENT_UID) docker compose up -d metamigo-postgresql signald

View file

@ -1,19 +1,36 @@
# Notes # Dev Setup
* Turbo https://turbo.build/repo > NOTE: When using Gitpod/Codespaces, use at least 16GB RAM
* Running dev in certain workspaces https://turbo.build/repo/docs/handbook/dev#running-dev-only-in-certain-workspaces
* Linting https://turbo.build/repo/docs/handbook/linting
* Internal packages https://turbo.build/repo/docs/handbook/sharing-code/internal-packages
``` Local dev with docker-compose
npm i
npm ls --production --depth 1 -json | jq -r '.dependencies[].resolved'
npm install --workspace=metamigo-common
```
## Todo * Create `link-stack/.env` from Bitwarden `.env for root of link-stack`
* Run local dev with docker-compose:
```
git clone ...
cd link-stack
make start-dev
```
* Move the following to be internal packages. Or for local dev of a single app
- [ ] @digiresilence/montar
- [ ] @digiresilience/hapi-nextauth * Create `link-stack/apps/link/.env.local` from Bitwarden `.env.local for link-stack/apps/link`
- [ ] @digiresilience/hapi-pg-promise * Create `link-stack/apps/metamigo-frontend/.metamigo.local.json` from Bitwarden `.metamigo.local.json for link-stack/apps/metamigo/frontend`
* Build locally for development:
```
npm install
make dev-metamigo # this starts the containers
npm run migrate # this migrates the db
npm run dev:metamigo # this runs metamigo frontend and api
```
# TODO
- [ ] Delete old JWT config stuff
- [ ] Consolidate config
- [ ] Complete react-admin upgrade.. make all the metamigo-frontend stuff work
* https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components
- [ ] Get metamigo-worker working
- [ ] Migrate off mui/styles
* https://mui.com/material-ui/migration/v5-style-changes/
* the codemods might help us?

View file

@ -175,7 +175,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const urgentCount = findOverviewCountByID(7); const urgentCount = findOverviewCountByID(7);
const pendingCount = findOverviewCountByID(3); const pendingCount = findOverviewCountByID(3);
const unassignedCount = findOverviewCountByID(2); const unassignedCount = findOverviewCountByID(2);
console.log({ assignedCount, urgentCount, pendingCount, unassignedCount });
const logout = () => { const logout = () => {
signOut({ callbackUrl: "/login" }); 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", "version": "0.2.0",
"main": "build/main/cli/index.js", "type": "module",
"main": "build/main/main.js",
"author": "Abel Luck <abel@guardianproject.info>", "author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
@ -26,8 +27,8 @@
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"graphile-migrate": "^1.4.1", "graphile-migrate": "^1.4.1",
"graphile-worker": "^0.13.0", "graphile-worker": "^0.13.0",
"hapi-auth-bearer-token": "^8.0.0",
"hapi-auth-jwt2": "^10.4.0", "hapi-auth-jwt2": "^10.4.0",
"hapi-postgraphile": "^0.11.0",
"hapi-swagger": "^16.0.1", "hapi-swagger": "^16.0.1",
"joi": "^17.9.2", "joi": "^17.9.2",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
@ -37,6 +38,7 @@
"pg": "^8.11.0", "pg": "^8.11.0",
"pg-monitor": "^2.0.0", "pg-monitor": "^2.0.0",
"pg-promise": "^11.4.3", "pg-promise": "^11.4.3",
"postgraphile": "4.12.3",
"postgraphile-plugin-connection-filter": "^2.3.0", "postgraphile-plugin-connection-filter": "^2.3.0",
"remeda": "^1.18.1", "remeda": "^1.18.1",
"twilio": "^4.11.1", "twilio": "^4.11.1",
@ -53,6 +55,7 @@
"pg-monitor": "^2.0.0", "pg-monitor": "^2.0.0",
"pino-pretty": "^10.0.0", "pino-pretty": "^10.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsc-watch": "^6.0.4",
"tsconfig-link": "*", "tsconfig-link": "*",
"typedoc": "^0.24.7", "typedoc": "^0.24.7",
"typescript": "^5.0.4" "typescript": "^5.0.4"
@ -75,6 +78,7 @@
"serve:prod": "NODE_ENV=production npm run cli server", "serve:prod": "NODE_ENV=production npm run cli server",
"worker": "NODE_ENV=development npm run cli worker", "worker": "NODE_ENV=development npm run cli worker",
"worker:prod": "NODE_ENV=production 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 type * as Hapi from "@hapi/hapi";
import * as Joi from "joi"; import Joi from "joi";
import type { IAppConfig } from "../config"; import type { IAppConfig } from "../config.js";
import * as Services from "./services"; import * as Services from "./services/index.js";
import * as Routes from "./routes"; import * as Routes from "./routes/index.js";
import * as Plugins from "./plugins"; import * as Plugins from "./plugins/index.js";
const AppPlugin = { const AppPlugin = {
name: "App", 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, server: Hapi.Server,
config: IAppConfig config: IAppConfig
): Promise<void> => { ): 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({ await server.register({
plugin: NextAuthPlugin, 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 type { IAppConfig } from "../../config";
import { dbInitOptions, IRepositories } from "@digiresilience/metamigo-db"; import { dbInitOptions, IRepositories } from "@digiresilience/metamigo-db";
import { registerNextAuth } from "./hapi-nextauth"; import { registerNextAuth } from "./hapi-nextauth.js";
import { registerSwagger } from "./swagger"; import { registerSwagger } from "./swagger.js";
import { registerNextAuthJwt } from "./nextauth-jwt"; import { registerCloudflareAccessJwt } from "./cloudflare-jwt.js";
import { registerCloudflareAccessJwt } from "./cloudflare-jwt"; import { registerAuthBearer } from "./auth-bearer.js";
import pg from "pg-promise/typescript/pg-subset"; import pg from "pg-promise/typescript/pg-subset";
import { registerPostgraphile } from "./hapi-postgraphile.js";
export const register = async ( export const register = async (
server: Hapi.Server, server: Hapi.Server,
config: IAppConfig config: IAppConfig
@ -34,6 +36,7 @@ export const register = async (
await registerNextAuth(server, config); await registerNextAuth(server, config);
await registerSwagger(server); await registerSwagger(server);
await registerNextAuthJwt(server, config);
await registerCloudflareAccessJwt(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({ export const withDefaults = Toys.withRouteDefaults({
options: { options: {
cors: true, cors: true,
auth: "nextauth-jwt", auth: "session-id-bearer-token",
validate: { validate: {
failAction: Metamigo.validatingFailAction, 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 type * as Hapi from "@hapi/hapi";
import * as UserRoutes from "./users"; import * as UserRoutes from "./users/index.js";
import * as VoiceRoutes from "./voice"; import * as VoiceRoutes from "./voice/index.js";
import * as WhatsappRoutes from "./whatsapp"; import * as WhatsappRoutes from "./whatsapp/index.js";
import * as SignalRoutes from "./signal"; import * as SignalRoutes from "./signal/index.js";
const loadRouteIndex = async (server, index) => { const loadRouteIndex = async (server, index) => {
const routes = []; const routes = [];

View file

@ -1,6 +1,6 @@
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import * as Joi from "joi"; import Joi from "joi";
import * as Helpers from "../helpers"; import * as Helpers from "../helpers/index.js";
import Boom from "@hapi/boom"; import Boom from "@hapi/boom";
const getSignalService = (request) => request.services("app").signaldService; 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 * as Hapi from "@hapi/hapi";
import { import {
UserRecord, UserRecord,
crudRoutesFor, crudRoutesFor,
CrudControllerBase, CrudControllerBase,
} from "@digiresilience/metamigo-common"; } from "@digiresilience/metamigo-common";
import * as RouteHelpers from "../helpers"; import * as RouteHelpers from "../helpers/index.js";
class UserRecordController extends CrudControllerBase(UserRecord) {} class UserRecordController extends CrudControllerBase(UserRecord) {}

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import {
ClientMessageWrapperv1, ClientMessageWrapperv1,
} from "@digiresilience/node-signald"; } from "@digiresilience/node-signald";
import { SavedSignalBot as Bot } from "@digiresilience/metamigo-db"; 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 { export default class SignaldService extends Service {
signald: SignaldAPI; signald: SignaldAPI;

View file

@ -15,7 +15,7 @@ import makeWASocket, {
useMultiFileAuthState, useMultiFileAuthState,
} from "@adiwajshing/baileys"; } from "@adiwajshing/baileys";
import fs from "fs"; import fs from "fs";
import workerUtils from "../../worker-utils"; import workerUtils from "../../worker-utils.js";
export type AuthCompleteCallback = (error?: string) => void; 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 * as Metamigo from "@digiresilience/metamigo-common";
import { defState } from "@digiresilience/montar"; import { defState } from "@digiresilience/montar";
import Manifest from "./manifest"; import Manifest from "./manifest.js";
import config, { IAppConfig } from "../config"; import config, { IAppConfig } from "../config.js";
export const deployment = async ( export const deployment = async (
config: IAppConfig, config: IAppConfig,

View file

@ -2,11 +2,8 @@ import * as Glue from "@hapi/glue";
import * as Metamigo from "@digiresilience/metamigo-common"; import * as Metamigo from "@digiresilience/metamigo-common";
import * as Blipp from "blipp"; import * as Blipp from "blipp";
import HapiBasic from "@hapi/basic"; import HapiBasic from "@hapi/basic";
import HapiJwt from "hapi-auth-jwt2"; import AppPlugin from "../app/index.js";
import HapiPostgraphile from "hapi-postgraphile"; import type { IAppConfig } from "../config.js";
import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
import AppPlugin from "../app";
import type { IAppConfig } from "../config";
const build = async (config: IAppConfig): Promise<Glue.Manifest> => { const build = async (config: IAppConfig): Promise<Glue.Manifest> => {
const { port, address } = config.server; const { port, address } = config.server;
@ -24,9 +21,6 @@ const build = async (config: IAppConfig): Promise<Glue.Manifest> => {
}, },
register: { register: {
plugins: [ plugins: [
// jwt plugin, required for our jwt auth plugin
{ plugin: HapiJwt },
// Blipp prints the nicely formatted list of endpoints at app boot // Blipp prints the nicely formatted list of endpoints at app boot
{ plugin: Blipp }, { plugin: Blipp },
@ -43,30 +37,6 @@ const build = async (config: IAppConfig): Promise<Glue.Manifest> => {
config, 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 * as Worker from "graphile-worker";
import { defState } from "@digiresilience/montar"; import { defState } from "@digiresilience/montar";
import config from "./config"; import config from "./config.js";
const startWorkerUtils = async (): Promise<Worker.WorkerUtils> => { const startWorkerUtils = async (): Promise<Worker.WorkerUtils> => {
const workerUtils = await Worker.makeWorkerUtils({ const workerUtils = await Worker.makeWorkerUtils({

View file

@ -5,8 +5,18 @@
"rootDir": "src", "rootDir": "src",
"skipLibCheck": true, "skipLibCheck": true,
"types": ["jest", "node", "long"], "types": ["jest", "node", "long"],
"lib": ["es2020", "DOM"] "lib": ["es2020", "DOM"],
"composite": true,
}, },
"include": ["src/**/*.ts", "src/**/.*.ts"], "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 set -e
cd ${AMIGO_DIR}
if [[ "$1" == "api" ]]; then if [[ "$1" == "api" ]]; then
echo "docker-entrypoint: starting api server" echo "docker-entrypoint: starting api server"
./cli db -- migrate ./cli db -- migrate
@ -10,9 +8,6 @@ if [[ "$1" == "api" ]]; then
elif [[ "$1" == "worker" ]]; then elif [[ "$1" == "worker" ]]; then
echo "docker-entrypoint: starting worker" echo "docker-entrypoint: starting worker"
exec dumb-init ./cli 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 elif [[ "$1" == "cli" ]]; then
echo "docker-entrypoint: starting frontend" echo "docker-entrypoint: starting frontend"
shift 1 shift 1

View file

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

View file

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

View file

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

View file

@ -4,12 +4,11 @@ import { Command } from "commander";
import { startWithout } from "@digiresilience/montar"; import { startWithout } from "@digiresilience/montar";
import { migrateWrapper } from "@digiresilience/metamigo-db"; import { migrateWrapper } from "@digiresilience/metamigo-db";
import { loadConfig } from "@digiresilience/metamigo-config"; import { loadConfig } from "@digiresilience/metamigo-config";
import { genConf, listConfig } from "./config"; import { genConf, listConfig } from "./config.js";
import { createTokenForTesting, generateJwks } from "./jwks"; import { createTokenForTesting, generateJwks } from "./jwks.js";
import { exportGraphqlSchema } from "./metamigo-postgraphile"; import { exportGraphqlSchema } from "./metamigo-postgraphile.js";
import "api/build/main/server"; import "@digiresilience/metamigo-api";
import "api/build/main/logger"; import "@digiresilience/metamigo-worker";
import "worker/build/main";
const program = new Command(); const program = new Command();

View file

@ -7,10 +7,12 @@ import {
printSchema, printSchema,
} from "graphql"; } from "graphql";
import { createPostGraphileSchema } from "postgraphile"; import { createPostGraphileSchema } from "postgraphile";
import { Pool } from "pg"; import pg from "pg";
import { loadConfig } from "@digiresilience/metamigo-config"; import { loadConfig } from "@digiresilience/metamigo-config";
import { getPostGraphileOptions } from "@digiresilience/metamigo-db"; import { getPostGraphileOptions } from "@digiresilience/metamigo-db";
const { Pool } = pg;
export const exportGraphqlSchema = async (): Promise<void> => { export const exportGraphqlSchema = async (): Promise<void> => {
const config = await loadConfig(); const config = await loadConfig();

View file

@ -1,13 +1,13 @@
{ {
"extends": "tsconfig-link", "extends": "tsconfig-link",
"compilerOptions": { "compilerOptions": {
"lib": ["es2020", "DOM"],
"incremental": true, "incremental": true,
"outDir": "build/main", "outDir": "build/main",
"rootDir": "src", "rootDir": "src",
"baseUrl": "./", "baseUrl": "./",
"skipLibCheck": true, "skipLibCheck": true,
"types": ["jest", "node"] "types": ["jest", "node"],
"esModuleInterop": true
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules/**"] "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 FROM base AS builder
RUN mkdir -p ${METAMIGO_DIR}/ ARG APP_DIR=/opt/metamigo-frontend
WORKDIR ${METAMIGO_DIR} RUN mkdir -p ${APP_DIR}/
COPY package.json tsconfig.json ${METAMIGO_DIR}/ RUN npm i -g turbo
COPY . ${METAMIGO_DIR}/ 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 COPY --from=builder ${APP_DIR}/out/full/ .
ARG METAMIGO_DIR=/opt/metamigo RUN npm i -g turbo
RUN turbo run build --filter=metamigo-frontend
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>"
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 && \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends --fix-missing \ apt-get install -y --no-install-recommends \
postgresql-client dumb-init ffmpeg dumb-init
RUN mkdir -p ${APP_DIR}
ARG METAMIGO_DIR=/opt/metamigo RUN chown -R node ${APP_DIR}/
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 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 3000
EXPOSE 3001
EXPOSE 3002
ENV PORT 3000 ENV PORT 3000
ENV NODE_ENV production ENV NODE_ENV production
ENTRYPOINT ["/opt/metamigo-frontend/apps/metamigo-frontend/docker-entrypoint.sh"]
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,3 +1,3 @@
export {default as AppBar} from "./AppBar"; export { default as AppBar } from "./AppBar";
export {default as Layout} from "./Layout"; export { default as Layout } from "./Layout";
export {default as Menu} from "./Menu"; export { default as Menu } from "./Menu";

View file

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

View file

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

View file

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

View file

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

View file

@ -6,11 +6,10 @@ import {
TextField, TextField,
EmailField, EmailField,
BooleanField, BooleanField,
ListProps,
} from "react-admin"; } from "react-admin";
const UserList = (props: ListProps) => ( const UserList = () => (
<List {...props} exporter={false}> <List exporter={false}>
<Datagrid rowClick="edit"> <Datagrid rowClick="edit">
<EmailField source="email" /> <EmailField source="email" />
<DateField source="emailVerified" /> <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) => ( export const UserRoleInput = (props: any) => {
<SelectInput const record = useRecordContext();
source="userRole" return (
choices={[ <SelectInput
{ id: "NONE", name: "None" }, source="userRole"
{ id: "USER", name: "User" }, choices={[
{ id: "ADMIN", name: "Admin" }, { id: "NONE", name: "None" },
]} { id: "USER", name: "User" },
disabled={props.session.user.id === props.record.id} { id: "ADMIN", name: "Admin" },
{...props} ]}
/> disabled={props.session.user.id === record.id}
); {...props}
/>
);
};

View file

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

View file

@ -1,23 +1,5 @@
#!/bin/bash #!/bin/bash
set -e set -e
echo "starting leafcutter"
cd ${AMIGO_DIR} exec dumb-init npm run start
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

@ -25,8 +25,7 @@ const customEnglishMessages: TranslationMessages = {
signalBots: { signalBots: {
name: "Signal Bot |||| Signal Bots", name: "Signal Bot |||| Signal Bots",
verifyDialog: { verifyDialog: {
sms: sms: "Please enter the verification code sent via SMS to %{phoneNumber}",
"Please enter the verification code sent via SMS to %{phoneNumber}",
voice: voice:
"Please answer the call from Signal to %{phoneNumber} and enter the verification code", "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 cloudflareAccountProvider = "cloudflare-access";
const cloudflareAuthorizeCallback = ( const cloudflareAuthorizeCallback =
req: IncomingMessage, (
domain: string, req: IncomingMessage,
verifier: VerifyFn, domain: string,
adapter: Adapter verifier: VerifyFn,
): (() => Promise<any>) => async () => { adapter: Adapter
/* ): (() => Promise<any>) =>
async () => {
/*
lots of little variables in here. 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 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 = { const profile = {
email: undefined, email: undefined,
name: undefined, name: undefined,
avatar: 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 * @param audience the cloudflare access audience id

View file

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

View file

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

View file

@ -60,38 +60,20 @@ const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => {
return { return {
secret: nextAuth.secret, secret: nextAuth.secret,
session: { session: {
jwt: true, strategy: "database",
maxAge: 8 * 60 * 60, // 8 hours maxAge: 8 * 60 * 60, // 8 hours
}, },
jwt: { jwt: {
secret: nextAuth.secret, secret: nextAuth.secret,
encryption: false,
signingKey: nextAuth.signingKey,
encryptionKey: nextAuth.encryptionKey,
}, },
providers, providers,
adapter, adapter,
callbacks: { callbacks: {
async session(session: any, token: any) { async session({ session, user }: any) {
// make the user id available in the react client session.user.id = user.id;
session.user.id = token.userId; session.user.userRole = user.userRole;
return session; 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: target:
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? "http://metamigo-api:3001" ? "http://metamigo-api:3001"
: "http://localhost:3001", : "http://127.0.0.1:3001",
changeOrigin: true, changeOrigin: true,
pathRewrite: { "^/graphql": "/graphql" }, pathRewrite: { "^/graphql": "/graphql" },
xfwd: true, xfwd: true,
@ -20,8 +20,6 @@ export default createProxyMiddleware({
let token = req.cookies["__Secure-next-auth.session-token"]; let token = req.cookies["__Secure-next-auth.session-token"];
if (!token) token = req.cookies["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) { if (token) {
proxyReq.setHeader("authorization", `Bearer ${token}`); proxyReq.setHeader("authorization", `Bearer ${token}`);
proxyReq.removeHeader("cookie"); 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 { parseCronItems } from "graphile-worker";
import { defState } from "@digiresilience/montar"; import { defState } from "@digiresilience/montar";
import config from "@digiresilience/metamigo-config"; import config from "@digiresilience/metamigo-config";
import { initPgp } from "./db"; import { initPgp } from "./db.js";
import logger from "./logger"; import logger from "./logger.js";
import workerUtils from "./utils"; import workerUtils from "./utils.js";
import { assertFfmpegAvailable } from "./lib/media-convert"; import { assertFfmpegAvailable } from "./lib/media-convert.js";
const logFactory = (scope: any) => (level: any, message: any, meta: any) => { const logFactory = (scope: any) => (level: any, message: any, meta: any) => {
const pinoLevel = level === "warning" ? "warn" : level; const pinoLevel = level === "warning" ? "warn" : level;

View file

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

View file

@ -1,5 +1,9 @@
version: "3.4" version: "3.4"
x-global-vars:
&common-global-variables
TZ: Etc/UTC
x-zammad-vars: x-zammad-vars:
&common-zammad-variables &common-zammad-variables
MEMCACHE_SERVERS: "zammad-memcached:11211" MEMCACHE_SERVERS: "zammad-memcached:11211"
@ -41,10 +45,12 @@ services:
zammad-elasticsearch: zammad-elasticsearch:
container_name: zammad-elasticsearch container_name: zammad-elasticsearch
environment: environment:
- discovery.type=single-node discovery.type: single-node
- ES_JAVA_OPTS=-Xms750m -Xmx750m ES_JAVA_OPTS: -Xms750m -Xmx750m
- xpack.security.enabled=false xpack.security.enabled: false
<<: *common-global-variables
build: ./docker/elasticsearch build: ./docker/elasticsearch
image: registry.gitlab.com/digiresilience/link/link-stack/zammad-elasticsearch
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:
- elasticsearch-data:/usr/share/elasticsearch/data - elasticsearch-data:/usr/share/elasticsearch/data
@ -56,10 +62,11 @@ services:
depends_on: depends_on:
- zammad-postgresql - zammad-postgresql
environment: environment:
<<: *common-zammad-variables <<: [ *common-zammad-variables, *common-global-variables ]
POSTGRESQL_USER: zammad POSTGRESQL_USER: zammad
POSTGRESQL_PASS: ${ZAMMAD_DATABASE_PASSWORD} POSTGRESQL_PASS: ${ZAMMAD_DATABASE_PASSWORD}
build: ./docker/zammad build: ./docker/zammad
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
restart: on-failure restart: on-failure
volumes: volumes:
- zammad-data:/opt/zammad - zammad-data:/opt/zammad
@ -68,7 +75,10 @@ services:
container_name: zammad-memcached container_name: zammad-memcached
command: memcached -m 256M command: memcached -m 256M
build: ./docker/memcached build: ./docker/memcached
image: registry.gitlab.com/digiresilience/link/link-stack/zammad-memcached
restart: ${RESTART} restart: ${RESTART}
environment:
<<: *common-global-variables
zammad-nginx: zammad-nginx:
platform: linux/x86_64 platform: linux/x86_64
@ -81,22 +91,27 @@ services:
depends_on: depends_on:
- zammad-railsserver - zammad-railsserver
build: ./docker/zammad build: ./docker/zammad
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
restart: ${RESTART} restart: ${RESTART}
environment: environment:
<<: *common-global-variables
NGINX_SERVER_SCHEME: https NGINX_SERVER_SCHEME: https
VIRTUAL_HOST: ${ZAMMAD_VIRTUAL_HOST} VIRTUAL_HOST: ${ZAMMAD_VIRTUAL_HOST}
VIRTUAL_PORT: 8080 VIRTUAL_PORT: 8080
volumes: volumes:
- zammad-data:/opt/zammad - zammad-data:/opt/zammad
zammad-postgresql: zammad-postgresql:
container_name: zammad-postgresql container_name: zammad-postgresql
environment: environment:
- POSTGRES_USER=zammad <<: *common-global-variables
- POSTGRES_PASSWORD=${ZAMMAD_DATABASE_PASSWORD} POSTGRES_USER: zammad
POSTGRES_PASSWORD: ${ZAMMAD_DATABASE_PASSWORD}
build: ./docker/postgresql build: ./docker/postgresql
image: registry.gitlab.com/digiresilience/link/link-stack/postgresql
restart: ${RESTART} restart: ${RESTART}
ports:
- 127.0.0.1:5432:5432
volumes: volumes:
- postgresql-data:/var/lib/postgresql/data - postgresql-data:/var/lib/postgresql/data
@ -108,8 +123,10 @@ services:
- zammad-memcached - zammad-memcached
- zammad-postgresql - zammad-postgresql
- zammad-redis - zammad-redis
environment: *common-zammad-variables environment:
<<: [ *common-global-variables, *common-zammad-variables ]
build: ./docker/zammad build: ./docker/zammad
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:
- zammad-data:/opt/zammad - zammad-data:/opt/zammad
@ -117,7 +134,10 @@ services:
zammad-redis: zammad-redis:
container_name: zammad-redis container_name: zammad-redis
build: ./docker/redis build: ./docker/redis
image: registry.gitlab.com/digiresilience/link/link-stack/zammad-redis
restart: ${RESTART} restart: ${RESTART}
environment:
<<: *common-global-variables
zammad-scheduler: zammad-scheduler:
platform: linux/x86_64 platform: linux/x86_64
@ -127,8 +147,10 @@ services:
- zammad-memcached - zammad-memcached
- zammad-railsserver - zammad-railsserver
- zammad-redis - zammad-redis
environment: *common-zammad-variables environment:
<<: [ *common-global-variables, *common-zammad-variables ]
build: ./docker/zammad build: ./docker/zammad
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:
- zammad-data:/opt/zammad - zammad-data:/opt/zammad
@ -141,67 +163,100 @@ services:
- zammad-memcached - zammad-memcached
- zammad-railsserver - zammad-railsserver
- zammad-redis - zammad-redis
environment: *common-zammad-variables environment:
<<: [ *common-global-variables, *common-zammad-variables ]
build: ./docker/zammad build: ./docker/zammad
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:
- zammad-data:/opt/zammad - zammad-data:/opt/zammad
# metamigo-api: opensearch:
# build: ./apps/metamigo-api container_name: opensearch
# container_name: metamigo-api build: ./docker/opensearch
# restart: ${RESTART} restart: ${RESTART}
# command: [ "api" ] volumes:
# expose: - opensearch-data:/usr/share/opensearch/data
# - "3001"
# environment: *common-metamigo-variables
# volumes:
# - ./signald:/signald
# metamigo-frontend: opensearch-dashboards:
# build: ./apps/metamigo-frontend container_name: opensearch-dashboards
# container_name: metamigo-frontend build: ./docker/opensearch-dashboards
# restart: ${RESTART} restart: ${RESTART}
# command: [ "frontend" ]
# expose:
# - "3000"
# ports:
# - 127.0.0.1:8002:3000
# environment:
# <<: *common-metamigo-variables
# VIRTUAL_HOST: ${METAMIGO_VIRTUAL_HOST}
# VIRTUAL_PORT: 3000
# metamigo-worker: metamigo-postgresql:
# build: ./apps/metamigo-worker build: ./docker/postgresql
# container_name: metamigo-worker image: registry.gitlab.com/digiresilience/link/link-stack/postgresql
# restart: ${RESTART} container_name: metamigo-postgresql
# command: [ "worker" ] restart: ${RESTART}
# environment: *common-metamigo-variables volumes:
- metamigo-data:/var/lib/postgresql/data
- ./scripts/bootstrap-metamigo.sh:/docker-entrypoint-initdb.d/bootstrap-metamigo.sh
environment:
<<: *common-metamigo-variables
POSTGRES_PASSWORD: ${METAMIGO_DATABASE_ROOT_PASSWORD}
POSTGRES_USER: "root"
POSTGRES_DB: "metamigo"
ports:
- 127.0.0.1:5433:5432
# metamigo-postgresql: metamigo-api:
# build: ./docker/postgresql build:
# restart: ${RESTART} context: .
# volumes: dockerfile: ./apps/metamigo-cli/Dockerfile
# - metamigo-data:/var/lib/postgresql/data image: registry.gitlab.com/digiresilience/link/link-stack/metamigo-cli
# - ./scripts/bootstrap-metamigo.sh:/docker-entrypoint-initdb.d/bootstrap-metamigo.sh container_name: metamigo-api
# environment: restart: ${RESTART}
# <<: *common-metamigo-variables command: [ "api" ]
# POSTGRES_PASSWORD: ${METAMIGO_DATABASE_ROOT_PASSWORD} ports:
# POSTGRES_USER: "root" - 127.0.0.1:8004:3001
# POSTGRES_DB: "metamigo" environment: *common-metamigo-variables
# expose: volumes:
# - "5432" - ./signald-state:/signald
# ports: depends_on:
# - 127.0.0.1:5432:5432 - metamigo-postgresql
- signald
metamigo-frontend:
build:
context: .
dockerfile: ./apps/metamigo-frontend/Dockerfile
image: registry.gitlab.com/digiresilience/link/link-stack/metamigo-frontend
container_name: metamigo-frontend
restart: ${RESTART}
command: [ "frontend" ]
expose:
- "3000"
ports:
- 127.0.0.1:8002:3000
depends_on:
- metamigo-api
environment:
<<: *common-metamigo-variables
VIRTUAL_HOST: ${METAMIGO_VIRTUAL_HOST}
VIRTUAL_PORT: 3000
metamigo-worker:
build:
context: .
dockerfile: ./apps/metamigo-cli/Dockerfile
image: registry.gitlab.com/digiresilience/link/link-stack/metamigo-cli
container_name: metamigo-worker
restart: ${RESTART}
command: [ "worker" ]
environment: *common-metamigo-variables
depends_on:
- metamigo-api
signald: signald:
container_name: signald container_name: signald
build: ./docker/signald build: ./docker/signald
image: registry.gitlab.com/digiresilience/link/link-stack/signald
restart: ${RESTART} restart: ${RESTART}
user: ${CURRENT_UID} user: ${CURRENT_UID}
volumes: volumes:
- ../signald:/signald - ./signald-state:/signald
environment:
<<: *common-global-variables
# nginx-proxy: # nginx-proxy:
# container_name: nginx-proxy # container_name: nginx-proxy
@ -265,3 +320,5 @@ volumes:
driver: local driver: local
metamigo-data: metamigo-data:
driver: local driver: local
opensearch-data:
driver: local

View file

@ -0,0 +1 @@
FROM opensearchproject/opensearch-dashboards:2.8.0

View file

@ -0,0 +1 @@
FROM opensearchproject/opensearch:2.8.0

View file

@ -2,3 +2,4 @@ docker-compose-test.yml
*.zpm *.zpm
*.zip *.zip
addons addons
!addons/.gitkeep

View file

6705
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,11 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "dotenv -- turbo run dev" "dev": "dotenv -- turbo run dev --concurrency 30",
"build": "turbo build --concurrency 30",
"dev:metamigo": "make dev-metamigo && dotenv -- turbo run dev --concurrency 30 --filter=!link --filter=!leafcutter",
"migrate": "dotenv -- npm run migrate --workspace=@digiresilience/metamigo-cli",
"fmt": "turbo run fmt"
}, },
"packageManager": "npm@9.3.1", "packageManager": "npm@9.3.1",
"workspaces": [ "workspaces": [
@ -16,7 +20,7 @@
"url": "git+https://gitlab.com/digiresilience/link/link-stack.git" "url": "git+https://gitlab.com/digiresilience/link/link-stack.git"
}, },
"author": "Darren Clarke", "author": "Darren Clarke",
"license": "ISC", "dlicense": "ISC",
"overrides": { "overrides": {
"@mui/styles": { "@mui/styles": {
"react": "18.2.0" "react": "18.2.0"
@ -24,11 +28,14 @@
"typeorm": { "typeorm": {
"pg": "^8.11.0" "pg": "^8.11.0"
}, },
"hapi-postgraphile": { "graphql": "15.8.0"
"pg": "^8.11.0"
}
}, },
"devDependencies": { "devDependencies": {
"prettier": "^2.8.8" "prettier": "^2.8.8",
"dotenv-cli": "latest"
},
"engines": {
"npm": ">=9.6.7",
"node": ">=20"
} }
} }

View file

@ -6,7 +6,7 @@ This is a plugin for hapi.js that exposes [NextAuth's database adapter](https://
```typescript ```typescript
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import * as Joi from "joi"; import Joi from "joi";
import NextAuthPlugin from "@digiresilience/hapi-nextauth"; import NextAuthPlugin from "@digiresilience/hapi-nextauth";
import type { AdapterInstance } from "next-auth/adapters"; import type { AdapterInstance } from "next-auth/adapters";

View file

@ -3,16 +3,18 @@
"version": "1.0.0", "version": "1.0.0",
"description": "a plugin for hapi.js that exposes NextAuth's database adapter via HTTP", "description": "a plugin for hapi.js that exposes NextAuth's database adapter via HTTP",
"main": "build/main/index.js", "main": "build/main/index.js",
"type": "module",
"author": "Abel Luck <abel@guardianproject.info>", "author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"private": false, "private": false,
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.1",
"@hapi/basic": "^7.0.1", "@hapi/basic": "^7.0.1",
"tsconfig-link": "*", "@types/jest": "^29.5.1",
"babel-preset-link": "*",
"eslint-config-link": "*", "eslint-config-link": "*",
"jest-config-link": "*", "jest-config-link": "*",
"babel-preset-link": "*" "tsc-watch": "^6.0.4",
"tsconfig-link": "*"
}, },
"dependencies": { "dependencies": {
"@hapi/hapi": "^21.3.2", "@hapi/hapi": "^21.3.2",
@ -28,6 +30,6 @@
"lint": "eslint src --ext .ts", "lint": "eslint src --ext .ts",
"lint-fmt": "prettier \"src/**/*.ts\" --list-different", "lint-fmt": "prettier \"src/**/*.ts\" --list-different",
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs", "doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
"watch:build": "tsc -p tsconfig.json -w" "dev": "tsc-watch --build --noClear"
} }
} }

View file

@ -1,9 +1,9 @@
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import * as Hoek from "@hapi/hoek"; import * as Hoek from "@hapi/hoek";
import * as Joi from "joi"; import Joi from "joi";
import { NextAuthPluginOptions } from "./types"; import { NextAuthPluginOptions } from "./types.js";
import * as Routes from "./routes"; import * as Routes from "./routes.js";
const minimumProfileSchema = Joi.object() const minimumProfileSchema = Joi.object()
.keys({ .keys({
@ -13,7 +13,7 @@ const minimumProfileSchema = Joi.object()
const minimumUserSchema = Joi.object() const minimumUserSchema = Joi.object()
.keys({ .keys({
id: Joi.string().required(), userId: Joi.string().required(),
email: Joi.string().email().required(), email: Joi.string().email().required(),
}) })
.unknown(true); .unknown(true);
@ -62,12 +62,11 @@ const register = async (
server: Hapi.Server, server: Hapi.Server,
pluginOpts?: any pluginOpts?: any
): Promise<void> => { ): Promise<void> => {
const options: any = const options: any = Hoek.applyToDefaults(
Hoek.applyToDefaults( // a little type gymnastics here to workaround poor typing
// a little type gymnastics here to workaround poor typing defaultOptions as any,
defaultOptions as any, pluginOpts
pluginOpts ) as any;
) as any;
if (!options.nextAuthAdapterFactory) { if (!options.nextAuthAdapterFactory) {
throw new Error( throw new Error(
@ -98,5 +97,5 @@ const nextAuthPlugin = {
version: "0.0.3", version: "0.0.3",
}; };
export * from "./types"; export * from "./types.js";
export default nextAuthPlugin; export default nextAuthPlugin;

View file

@ -1,5 +1,5 @@
/* eslint-disable unicorn/no-null */ /* eslint-disable unicorn/no-null */
import * as Joi from "joi"; import Joi from "joi";
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import { ResponseToolkit, ResponseObject } from "@hapi/hapi"; import { ResponseToolkit, ResponseObject } from "@hapi/hapi";
@ -93,13 +93,13 @@ export const register = async <TUser, TProfile>(
}, },
{ {
method: "GET", method: "GET",
path: `${basePath}/getUserByProviderAccountId/{providerId}/{providerAccountId}`, path: `${basePath}/getUserByAccount/{provider}/{providerAccountId}`,
options: { options: {
auth, auth,
tags, tags,
validate: { validate: {
params: { params: {
providerId: Joi.string(), provider: Joi.string(),
providerAccountId: Joi.string(), providerAccountId: Joi.string(),
}, },
}, },
@ -107,10 +107,10 @@ export const register = async <TUser, TProfile>(
request: Hapi.Request, request: Hapi.Request,
h: ResponseToolkit h: ResponseToolkit
): Promise<ResponseObject> { ): Promise<ResponseObject> {
const { providerId, providerAccountId } = request.params; const { provider, providerAccountId } = request.params;
const r = await opts const r = await opts
.nextAuthAdapterFactory(request) .nextAuthAdapterFactory(request)
.getUserByProviderAccountId(providerId, providerAccountId); .getUserByAccount(provider, providerAccountId);
if (!r) return h.response().code(404); if (!r) return h.response().code(404);
return h.response(r as object); return h.response(r as object);
}, },
@ -148,14 +148,15 @@ export const register = async <TUser, TProfile>(
tags, tags,
validate: { validate: {
payload: Joi.object({ payload: Joi.object({
userId, // https://next-auth.js.org/getting-started/upgrade-v4#schema-changes
providerId: Joi.string(), userId: Joi.string().required(),
providerType: Joi.string(), provider: Joi.string().required(),
providerAccountId: Joi.string(), type: Joi.string().required(),
refreshToken: Joi.string().optional().allow(null), providerAccountId: Joi.string().required(),
accessToken: Joi.string().optional().allow(null), refresh_token: Joi.string().optional().allow(null),
accessTokenExpires: Joi.number().optional().allow(null), access_token: Joi.string().optional().allow(null),
}).options({ presence: "required" }), expires_at: Joi.number().optional().allow(null),
}).unknown(true),
}, },
async handler( async handler(
request: Hapi.Request, request: Hapi.Request,
@ -193,7 +194,11 @@ export const register = async <TUser, TProfile>(
auth, auth,
tags, tags,
validate: { validate: {
payload: user, payload: Joi.object({
userId: Joi.string().required(),
sessionToken: Joi.string().required(),
expires: Joi.string().isoDate().required(),
}),
}, },
async handler( async handler(
request: Hapi.Request, request: Hapi.Request,
@ -210,7 +215,7 @@ export const register = async <TUser, TProfile>(
}, },
{ {
method: "GET", method: "GET",
path: `${basePath}/getSession/{sessionToken}`, path: `${basePath}/getSessionAndUser/{sessionToken}`,
options: { options: {
auth, auth,
tags, tags,
@ -226,7 +231,7 @@ export const register = async <TUser, TProfile>(
const token = request.params.sessionToken; const token = request.params.sessionToken;
const r = await opts const r = await opts
.nextAuthAdapterFactory(request) .nextAuthAdapterFactory(request)
.getSession(token); .getSessionAndUser(token);
if (!r) return h.response().code(404); if (!r) return h.response().code(404);
return h.response(r as object); return h.response(r as object);
}, },

View file

@ -2,9 +2,7 @@ import type { Adapter } from "next-auth/adapters";
import type { NumberSchema, StringSchema, ObjectSchema } from "joi"; import type { NumberSchema, StringSchema, ObjectSchema } from "joi";
import type { Request } from "@hapi/hapi"; import type { Request } from "@hapi/hapi";
export type AdapterFactory = ( export type AdapterFactory = (request: Request) => Adapter;
request: Request
) => Adapter;
export interface NextAuthPluginOptions { export interface NextAuthPluginOptions {
nextAuthAdapterFactory: Adapter; nextAuthAdapterFactory: Adapter;

View file

@ -1,6 +1,7 @@
{ {
"extends": "tsconfig-link", "extends": "tsconfig-link",
"compilerOptions": { "compilerOptions": {
"composite": true,
"incremental": true, "incremental": true,
"outDir": "build/main", "outDir": "build/main",
"rootDir": "src", "rootDir": "src",

View file

@ -3,10 +3,12 @@
"version": "1.0.0", "version": "1.0.0",
"description": "a hapi.js plugin for pg-promise", "description": "a hapi.js plugin for pg-promise",
"main": "build/main/index.js", "main": "build/main/index.js",
"type": "module",
"author": "Abel Luck <abel@guardianproject.info>", "author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"private": false, "private": false,
"devDependencies": { "devDependencies": {
"tsc-watch": "^6.0.4"
}, },
"dependencies": { "dependencies": {
"@hapi/hapi": "^21.3.2", "@hapi/hapi": "^21.3.2",
@ -20,6 +22,6 @@
"test": "jest --coverage --forceExit --detectOpenHandles --reporters=default --reporters=jest-junit", "test": "jest --coverage --forceExit --detectOpenHandles --reporters=default --reporters=jest-junit",
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different", "lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs", "doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
"watch:build": "tsc -p tsconfig.json -w" "dev": "tsc-watch --build --noClear"
} }
} }

View file

@ -1,12 +1,12 @@
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import pgPromise from "pg-promise"; import pgPromise from "pg-promise";
import * as pgMonitor from "pg-monitor"; import pgMonitor from "pg-monitor";
import type { IConnectionParameters } from "pg-promise/typescript/pg-subset"; import type { IConnectionParameters } from "pg-promise/typescript/pg-subset";
import type { IMain, IInitOptions } from "pg-promise"; import type { IMain, IInitOptions } from "pg-promise";
import { IPGPPluginOptions, ExtendedProtocol } from "./types"; import { IPGPPluginOptions, ExtendedProtocol } from "./types.js";
import { Plugin } from "@hapi/hapi/lib/types/plugin"; import { Plugin } from "@hapi/hapi/lib/types/plugin";
export * from "./types"; export * from "./types.js";
export const startDiagnostics = <T>( export const startDiagnostics = <T>(
logSql: boolean, logSql: boolean,

View file

@ -1,6 +1,7 @@
{ {
"extends": "tsconfig-link", "extends": "tsconfig-link",
"compilerOptions": { "compilerOptions": {
"composite": true,
"incremental": true, "incremental": true,
"outDir": "build/main", "outDir": "build/main",
"rootDir": "src", "rootDir": "src",

View file

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

View file

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

View file

@ -3,6 +3,7 @@
"version": "0.2.0", "version": "0.2.0",
"description": "", "description": "",
"main": "build/main/index.js", "main": "build/main/index.js",
"type": "module",
"types": "build/main/index.d.ts", "types": "build/main/index.d.ts",
"author": "Abel Luck <abel@guardianproject.info>", "author": "Abel Luck <abel@guardianproject.info>",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@ -13,7 +14,7 @@
"fmt": "prettier \"src/**/*.ts\" --write", "fmt": "prettier \"src/**/*.ts\" --write",
"lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different", "lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different",
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs", "doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
"watch:build": "tsc -p tsconfig.json -w" "dev": "tsc-watch --build --noClear "
}, },
"devDependencies": { "devDependencies": {
"@types/figlet": "^1.5.6", "@types/figlet": "^1.5.6",
@ -22,6 +23,7 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"camelcase-keys": "^8.0.2", "camelcase-keys": "^8.0.2",
"pg-monitor": "^2.0.0", "pg-monitor": "^2.0.0",
"tsc-watch": "^6.0.4",
"typedoc": "^0.24.7", "typedoc": "^0.24.7",
"typescript": "^5.0.4" "typescript": "^5.0.4"
}, },
@ -55,6 +57,7 @@
"next-auth": "^4.22.1", "next-auth": "^4.22.1",
"pg-promise": "^11.4.3", "pg-promise": "^11.4.3",
"pino": "^8.14.1", "pino": "^8.14.1",
"pino-pretty": "^10.0.0",
"prom-client": "^14.x.x", "prom-client": "^14.x.x",
"uuid": "^9.0.0" "uuid": "^9.0.0"
} }

View file

@ -1,4 +1,4 @@
import { ConvictSchema } from "./types"; import { ConvictSchema } from "./types.js";
export interface IAppMetaConfig { export interface IAppMetaConfig {
name: string; name: string;

View file

@ -1,4 +1,4 @@
import { ConvictSchema } from "./types"; import { ConvictSchema } from "./types.js";
export interface ISessionConfig { export interface ISessionConfig {
sessionMaxAgeSeconds: number; sessionMaxAgeSeconds: number;

View file

@ -1,4 +1,4 @@
import { ConvictSchema } from "./types"; import { ConvictSchema } from "./types.js";
export interface ICorsConfig { export interface ICorsConfig {
allowedMethods: Array<string>; allowedMethods: Array<string>;

View file

@ -1,4 +1,4 @@
import * as Joi from "joi"; import Joi from "joi";
import type { Format } from "convict"; import type { Format } from "convict";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -1,12 +1,12 @@
import process from "node:process"; import process from "node:process";
import convict, { SchemaObj } from "convict"; import convict, { SchemaObj } from "convict";
import { IServerConfig, ServerConfig } from "./server"; import { IServerConfig, ServerConfig } from "./server.js";
import { IMetricsConfig, MetricsConfig } from "./metrics-server"; import { IMetricsConfig, MetricsConfig } from "./metrics-server.js";
import { IAppMetaConfig, AppMetaConfig } from "./app-meta"; import { IAppMetaConfig, AppMetaConfig } from "./app-meta.js";
import { ICorsConfig, CorsConfig } from "./cors"; import { ICorsConfig, CorsConfig } from "./cors.js";
import { ILoggingConfig, LoggingConfig } from "./logging"; import { ILoggingConfig, LoggingConfig } from "./logging.js";
import { ExtendedConvict } from "./types"; import { ExtendedConvict } from "./types.js";
import { MetamigoConvictFormats } from "./formats"; import { MetamigoConvictFormats } from "./formats.js";
type IEnvConfig = "production" | "development" | "test"; type IEnvConfig = "production" | "development" | "test";
@ -51,10 +51,10 @@ export type IMetamigoConvict = ExtendedConvict<IMetamigoConfig>;
export type { IMetamigoConfig }; export type { IMetamigoConfig };
export * from "./formats"; export * from "./formats.js";
export * from "./generate"; export * from "./generate.js";
export * from "./print"; export * from "./print.js";
export * from "./types"; export * from "./types.js";
/** /**
* Loads your applications configuration from environment variables and configuration files (see METAMIGO_CONFIG). * Loads your applications configuration from environment variables and configuration files (see METAMIGO_CONFIG).
@ -137,8 +137,8 @@ export const loadConfiguration = async <T extends IMetamigoConfig>(
return c.getProperties(); return c.getProperties();
}; };
export { type IServerConfig } from "./server"; export { type IServerConfig } from "./server.js";
export { type IMetricsConfig } from "./metrics-server"; export { type IMetricsConfig } from "./metrics-server.js";
export { type IAppMetaConfig } from "./app-meta"; export { type IAppMetaConfig } from "./app-meta.js";
export { type ICorsConfig } from "./cors"; export { type ICorsConfig } from "./cors.js";
export { type ILoggingConfig } from "./logging"; export { type ILoggingConfig } from "./logging.js";

View file

@ -1,4 +1,4 @@
import { ConvictSchema } from "./types"; import { ConvictSchema } from "./types.js";
export interface ILoggingConfig { export interface ILoggingConfig {
level: string; level: string;

View file

@ -1,4 +1,4 @@
import { ConvictSchema } from "./types"; import { ConvictSchema } from "./types.js";
export interface IMetricsConfig { export interface IMetricsConfig {
address: string; address: string;

View file

@ -1,4 +1,4 @@
import { ConvictSchema } from "./types"; import { ConvictSchema } from "./types.js";
export interface IServerConfig { export interface IServerConfig {
address: string; address: string;

View file

@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any,max-params */ /* eslint-disable @typescript-eslint/no-explicit-any,max-params */
import * as Boom from "@hapi/boom"; import * as Boom from "@hapi/boom";
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import { CrudRepository } from "../records/crud-repository"; import { CrudRepository } from "../records/crud-repository.js";
import { createResponse } from "../helpers/response"; import { createResponse } from "../helpers/response.js";
import { import {
PgRecordInfo, PgRecordInfo,
UnsavedR, UnsavedR,
SavedR, SavedR,
KeyType, KeyType,
} from "../records/record-info"; } from "../records/record-info.js";
/** /**
* *

View file

@ -1,10 +1,16 @@
/* eslint-disable unicorn/no-null,max-params */ /* eslint-disable unicorn/no-null,max-params */
import { createHash, randomBytes } from "node:crypto"; import { createHash, randomBytes } from "node:crypto";
import omit from "lodash/omit"; import omit from "lodash/omit.js";
import type { IMetamigoRepositories } from "../records"; import { IMetamigoRepositories, idKeysOf } from "../records/index.js";
import type { UnsavedAccount } from "../records/account"; import type { UnsavedAccount } from "../records/account.js";
import type { UserId, UnsavedUser, SavedUser } from "../records/user"; import type { UserId, UnsavedUser, SavedUser } from "../records/user.js";
import type { UnsavedSession, SavedSession } from "../records/session"; import type { UnsavedSession, SavedSession } from "../records/session.js";
import {
AdapterAccount,
AdapterSession,
AdapterUser,
} from "next-auth/adapters.js";
import { ReadableStreamDefaultController } from "stream/web";
// Sessions expire after 30 days of being idle // Sessions expire after 30 days of being idle
export const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000; export const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000;
@ -23,7 +29,7 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
private repos: TRepositories, private repos: TRepositories,
private readonly sessionMaxAge = defaultSessionMaxAge, private readonly sessionMaxAge = defaultSessionMaxAge,
private readonly sessionUpdateAge = defaulteSessionUpdateAge private readonly sessionUpdateAge = defaulteSessionUpdateAge
) { } ) {}
async createUser(profile: UnsavedUser): Promise<SavedUser> { async createUser(profile: UnsavedUser): Promise<SavedUser> {
// @ts-expect-error Typescript doesn't like lodash's omit() // @ts-expect-error Typescript doesn't like lodash's omit()
@ -56,12 +62,12 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
return user; return user;
} }
async getUserByProviderAccountId( async getUserByAccount(
providerId: string, provider: string,
providerAccountId: string providerAccountId: string
): Promise<SavedUser | null> { ): Promise<SavedUser | null> {
const account = await this.repos.accounts.findBy({ const account = await this.repos.accounts.findBy({
compoundId: getCompoundId(providerId, providerAccountId), compoundId: getCompoundId(provider, providerAccountId),
}); });
if (!account) return null; if (!account) return null;
@ -72,15 +78,16 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
return this.repos.users.update(user); return this.repos.users.update(user);
} }
async linkAccount( async linkAccount(adapterAccount: AdapterAccount): Promise<void> {
userId: string, const {
providerId: string, userId,
providerType: string, access_token: accessToken,
providerAccountId: string, refresh_token: refreshToken,
refreshToken: string, provider: providerId,
accessToken: string, providerAccountId,
accessTokenExpires: number expires_at: accessTokenExpires,
): Promise<void> { type: providerType,
} = adapterAccount;
const exists = await this.repos.users.existsById({ id: userId }); const exists = await this.repos.users.existsById({ id: userId });
if (!exists) return; if (!exists) return;
const account: UnsavedAccount = { const account: UnsavedAccount = {
@ -109,7 +116,13 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
}); });
} }
createSession(user: SavedUser): Promise<SavedSession> { createSession({
sessionToken,
userId,
}: {
sessionToken: string;
userId: string;
}): Promise<SavedSession> {
let expires; let expires;
if (this.sessionMaxAge) { if (this.sessionMaxAge) {
const dateExpires = new Date(Date.now() + this.sessionMaxAge); const dateExpires = new Date(Date.now() + this.sessionMaxAge);
@ -118,22 +131,42 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
const session: UnsavedSession = { const session: UnsavedSession = {
expires, expires,
userId: user.id, userId,
sessionToken: randomToken(), sessionToken,
//sessionToken: randomToken(),
accessToken: randomToken(), accessToken: randomToken(),
}; };
return this.repos.sessions.insert(session); return this.repos.sessions.insert(session);
} }
async getSession(sessionToken: string): Promise<SavedSession | null> { async getSessionAndUser(
sessionToken: string
): Promise<{ session: AdapterSession; user: any } | null> {
const session = await this.repos.sessions.findBy({ sessionToken }); const session = await this.repos.sessions.findBy({ sessionToken });
if (!session) return null;
if (session && session.expires && new Date() > session.expires) { if (session && session.expires && new Date() > session.expires) {
this.repos.sessions.remove(session); this.repos.sessions.remove(session);
return null; return null;
} }
return session; const user = await this.repos.users.findById({ id: session.userId });
if (!user) return null;
const adapterSession: AdapterSession = {
userId: session.userId,
expires: session.expires,
sessionToken: sessionToken,
};
const adapterUser: any = {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
userRole: user.userRole
};
return { session: adapterSession, user: adapterUser };
} }
async updateSession( async updateSession(

View file

@ -8,11 +8,11 @@ import PinoPlugin from "hapi-pino";
import { createServer as createPrometheusServer } from "@promster/server"; import { createServer as createPrometheusServer } from "@promster/server";
import { createHttpTerminator } from "http-terminator"; import { createHttpTerminator } from "http-terminator";
import { getPrettyPrint } from "./logger"; import { configureLogger } from "./logger.js";
import RequestIdPlugin from "./plugins/request-id"; import RequestIdPlugin from "./plugins/request-id.js";
import StatusPlugin from "./plugins/status"; import StatusPlugin from "./plugins/status.js";
import ConfigPlugin from "./plugins/config"; import ConfigPlugin from "./plugins/config.js";
import { IMetamigoConfig } from "./config"; import { IMetamigoConfig } from "./config/index.js";
export interface Server { export interface Server {
hapiServer: Hapi.Server; hapiServer: Hapi.Server;
@ -79,8 +79,8 @@ export const defaultPlugins = <T extends IMetamigoConfig>(
{ {
plugin: PinoPlugin, plugin: PinoPlugin,
options: { options: {
prettyPrint: getPrettyPrint(config),
level, level,
instance: configureLogger(config),
logRequestStart, logRequestStart,
logRequestComplete, logRequestComplete,
logPayload: logRequestPayload, logPayload: logRequestPayload,

View file

@ -1,7 +1,7 @@
import process from "node:process"; import process from "node:process";
import * as Hapi from "@hapi/hapi"; import * as Hapi from "@hapi/hapi";
import * as Joi from "joi"; import Joi from "joi";
import Hoek from "@hapi/hoek"; import * as Hoek from "@hapi/hoek";
import * as Boom from "@hapi/boom"; import * as Boom from "@hapi/boom";
export interface HapiValidationError extends Joi.ValidationError { export interface HapiValidationError extends Joi.ValidationError {

View file

@ -1,12 +1,12 @@
export * from "./config"; export * from "./config/index.js";
export * from "./controllers/crud-controller"; export * from "./controllers/crud-controller.js";
export * from "./controllers/nextauth-adapter"; export * from "./controllers/nextauth-adapter.js";
export * from "./hapi"; export * from "./hapi.js";
export * from "./helpers"; export * from "./helpers/index.js";
export * from "./helpers/response"; export * from "./helpers/response.js";
export * from "./helpers/validation-error"; export * from "./helpers/validation-error.js";
export * from "./logger"; export * from "./logger.js";
export * from "./records"; export * from "./records/index.js";
import * as pino from "pino"; import * as pino from "pino";

View file

@ -1,13 +1,5 @@
import pino, { LoggerOptions } from "pino"; import pino, { LoggerOptions } from "pino";
import { IMetamigoConfig } from "./config"; import { IMetamigoConfig } from "./config/index.js";
export const getPrettyPrint = <T extends IMetamigoConfig>(
config: T
): boolean => {
const { prettyPrint } = config.logging;
if (prettyPrint === "auto") return config?.isDev || false;
return prettyPrint === true;
};
export const configureLogger = <T extends IMetamigoConfig>( export const configureLogger = <T extends IMetamigoConfig>(
config: T config: T
@ -15,6 +7,9 @@ export const configureLogger = <T extends IMetamigoConfig>(
const { level, redact } = config.logging; const { level, redact } = config.logging;
const options: LoggerOptions = { const options: LoggerOptions = {
level, level,
transport: {
target: "pino-pretty",
},
redact: { redact: {
paths: redact, paths: redact,
remove: true, remove: true,

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