metamigo: do nextauth v3 -> v4 upgrades

This commit is contained in:
Abel Luck 2023-06-06 10:28:29 +00:00
parent a33f80c497
commit 45f8cb1234
13 changed files with 158 additions and 123 deletions

View file

@ -40,4 +40,9 @@ npm install --workspace=metamigo-common
``` ```
turbo run build --filter metamigo-cli turbo run build --filter metamigo-cli
npm run dev -- --filter @digiresilience/metamigo-cli
npm run migrate
``` ```

View file

@ -11,6 +11,7 @@
"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-api": "*",
"@digiresilience/metamigo-worker": "*", "@digiresilience/metamigo-worker": "*",
@ -32,7 +33,9 @@
"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",
"dev": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js api",
"cli": "NODE_ENV=development node --unhandled-rejections=strict build/main/index.js", "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",

View file

@ -6,7 +6,7 @@ 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() as any; const c = (await loadConfigRaw()) as any;
const generated = generateConfig(c) as any; const generated = generateConfig(c) as any;
console.log(generated); console.log(generated);
}; };
@ -17,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() as any; const c = (await loadConfigRaw()) as any;
printConfigOptions(c); printConfigOptions(c);
}; };

View file

@ -11,7 +11,7 @@ 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; 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 +0,0 @@
METAMIGO_CONFIG=.metamigo.local.json

View file

@ -1,8 +1,5 @@
/* eslint-disable unicorn/no-null */ /* eslint-disable unicorn/no-null */
/* eslint-disable max-params */ import type { Adapter, AdapterAccount, AdapterSession, AdapterUser } from "next-auth/adapters";
import type { Adapter } from "next-auth/adapters";
// @ts-expect-error: Missing export
import type { AppOptions } from "next-auth";
import * as Wreck from "@hapi/wreck"; import * as Wreck from "@hapi/wreck";
import * as Boom from "@hapi/boom"; import * as Boom from "@hapi/boom";
@ -70,7 +67,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 +103,17 @@ export const MetamigoAdapter = (config: IAppConfig): Adapter => {
} }
} }
async function getUserByProviderAccountId( async function getUserByAccount({ providerAccountId, provider }: { providerAccountId: string, provider: string }) {
providerId: string,
providerAccountId: 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");
} }
} }
@ -135,51 +130,39 @@ export const MetamigoAdapter = (config: IAppConfig): Adapter => {
} }
async function linkAccount( async function linkAccount(
userId: string, account: AdapterAccount
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("createSession", {
payload: user, 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(`getSessionAndUser/${sessionToken}`);
return payload; 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 +196,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

@ -7,6 +7,7 @@ import Cognito from "next-auth/providers/cognito";
import { loadConfig, IAppConfig } from "@digiresilience/metamigo-config"; import { loadConfig, IAppConfig } from "@digiresilience/metamigo-config";
import { MetamigoAdapter } from "../../../lib/nextauth-adapter"; import { MetamigoAdapter } from "../../../lib/nextauth-adapter";
import { CloudflareAccessProvider } from "../../../lib/cloudflare"; import { CloudflareAccessProvider } from "../../../lib/cloudflare";
import { AdapterSession, AdapterUser } from "next-auth/adapters";
const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => { const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => {
const { nextAuth, cfaccess } = config; const { nextAuth, cfaccess } = config;
@ -72,11 +73,6 @@ const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => {
providers, providers,
adapter, adapter,
callbacks: { callbacks: {
async session(session: any, token: any) {
// make the user id available in the react client
session.user.id = token.userId;
return session;
},
async jwt(token: any, user: any) { async jwt(token: any, user: any) {
const isSignIn = Boolean(user); const isSignIn = Boolean(user);
// Add auth_time to token on signin in // Add auth_time to token on signin in

45
package-lock.json generated
View file

@ -13,6 +13,7 @@
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"dotenv-cli": "latest",
"prettier": "^2.8.8" "prettier": "^2.8.8"
} }
}, },
@ -8189,8 +8190,7 @@
"node_modules/colorette": { "node_modules/colorette": {
"version": "2.0.20", "version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
"dev": true
}, },
"node_modules/comma-separated-tokens": { "node_modules/comma-separated-tokens": {
"version": "2.0.3", "version": "2.0.3",
@ -8433,7 +8433,6 @@
"version": "4.6.3", "version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true,
"engines": { "engines": {
"node": "*" "node": "*"
} }
@ -8964,6 +8963,30 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/dotenv-cli": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz",
"integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.3",
"dotenv": "^16.0.0",
"dotenv-expand": "^10.0.0",
"minimist": "^1.2.6"
},
"bin": {
"dotenv": "cli.js"
}
},
"node_modules/dotenv-expand": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/duplexer2": { "node_modules/duplexer2": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@ -10233,8 +10256,7 @@
"node_modules/fast-copy": { "node_modules/fast-copy": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz",
"integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==", "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA=="
"dev": true
}, },
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
@ -10319,8 +10341,7 @@
"node_modules/fast-safe-stringify": { "node_modules/fast-safe-stringify": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
"dev": true
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.15.0", "version": "1.15.0",
@ -11635,7 +11656,6 @@
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz",
"integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==",
"dev": true,
"dependencies": { "dependencies": {
"glob": "^8.0.0", "glob": "^8.0.0",
"readable-stream": "^3.6.0" "readable-stream": "^3.6.0"
@ -11645,7 +11665,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
@ -11654,7 +11673,6 @@
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
"dev": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@ -11673,7 +11691,6 @@
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
}, },
@ -11685,7 +11702,6 @@
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@ -11699,7 +11715,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
@ -14216,7 +14231,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@ -16980,7 +16994,6 @@
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.0.0.tgz", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.0.0.tgz",
"integrity": "sha512-zKFjYXBzLaLTEAN1ayKpHXtL5UeRQC7R3lvhKe7fWs7hIVEjKGG/qIXwQt9HmeUp71ogUd/YcW+LmMwRp4KT6Q==", "integrity": "sha512-zKFjYXBzLaLTEAN1ayKpHXtL5UeRQC7R3lvhKe7fWs7hIVEjKGG/qIXwQt9HmeUp71ogUd/YcW+LmMwRp4KT6Q==",
"dev": true,
"dependencies": { "dependencies": {
"colorette": "^2.0.7", "colorette": "^2.0.7",
"dateformat": "^4.6.3", "dateformat": "^4.6.3",
@ -17005,7 +17018,6 @@
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.0.tgz",
"integrity": "sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==", "integrity": "sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==",
"dev": true,
"dependencies": { "dependencies": {
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
@ -21976,6 +21988,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

@ -4,7 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "dotenv -- turbo run dev" "dev": "dotenv -- turbo run dev",
"migrate": "dotenv -- npm run migrate --workspace=@digiresilience/metamigo-cli"
}, },
"packageManager": "npm@9.3.1", "packageManager": "npm@9.3.1",
"workspaces": [ "workspaces": [
@ -16,7 +17,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"
@ -29,6 +30,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"prettier": "^2.8.8" "prettier": "^2.8.8",
"dotenv-cli": "latest"
} }
} }

View file

@ -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);

View file

@ -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

@ -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.js"; import omit from "lodash/omit.js";
import type { IMetamigoRepositories } from "../records/index.js"; import { IMetamigoRepositories, idKeysOf } from "../records/index.js";
import type { UnsavedAccount } from "../records/account.js"; import type { UnsavedAccount } from "../records/account.js";
import type { UserId, UnsavedUser, SavedUser } from "../records/user.js"; import type { UserId, UnsavedUser, SavedUser } from "../records/user.js";
import type { UnsavedSession, SavedSession } from "../records/session.js"; 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,41 @@ 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: AdapterUser } | 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: AdapterUser = {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
};
return { session: adapterSession, user: adapterUser };
} }
async updateSession( async updateSession(

View file

@ -9,20 +9,20 @@ export const configSchema = {
doc: "The postgres connection url.", doc: "The postgres connection url.",
format: "uri", format: "uri",
default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_dev", default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_dev",
env: "DATABASE_URL", env: "METAMIGO_DATABASE_URL",
sensitive: true, sensitive: true,
}, },
name: { name: {
doc: "The name of the postgres database", doc: "The name of the postgres database",
format: String, format: String,
default: "metamigo_dev", default: "metamigo_dev",
env: "DATABASE_NAME", env: "METAMIGO_DATABASE_NAME",
}, },
owner: { owner: {
doc: "The username of the postgres database owner", doc: "The username of the postgres database owner",
format: String, format: String,
default: "metamigo", default: "metamigo",
env: "DATABASE_OWNER", env: "METAMIGO_DATABASE_OWNER",
}, },
}, },
worker: { worker: {
@ -30,19 +30,19 @@ export const configSchema = {
doc: "The postgres connection url for the worker database.", doc: "The postgres connection url for the worker database.",
format: "uri", format: "uri",
default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_dev", default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_dev",
env: "WORKER_DATABASE_URL", env: "METAMIGO_WORKER_DATABASE_URL",
}, },
concurrency: { concurrency: {
doc: "The number of jobs to run concurrently", doc: "The number of jobs to run concurrently",
default: 1, default: 1,
format: "positiveInt", format: "positiveInt",
env: "WORKER_CONCURRENT_JOBS", env: "METAMIGO_WORKER_CONCURRENT_JOBS",
}, },
pollInterval: { pollInterval: {
doc: "How long to wait between polling for jobs in milliseconds (for jobs scheduled in the future/retries)", doc: "How long to wait between polling for jobs in milliseconds (for jobs scheduled in the future/retries)",
default: 2000, default: 2000,
format: "positiveInt", format: "positiveInt",
env: "WORKER_POLL_INTERVAL_MS", env: "METAMIGO_WORKER_POLL_INTERVAL_MS",
}, },
}, },
postgraphile: { postgraphile: {
@ -50,26 +50,26 @@ export const configSchema = {
doc: "The postgres role that postgraphile logs in with", doc: "The postgres role that postgraphile logs in with",
format: String, format: String,
default: "metamigo_graphile_auth", default: "metamigo_graphile_auth",
env: "DATABASE_AUTHENTICATOR", env: "METAMIGO_DATABASE_AUTHENTICATOR",
}, },
appRootConnection: { appRootConnection: {
doc: "The postgres root/superuser connection url for development mode so PG can watch the schema changes, this is strangely named in the postgraphile API 'ownerConnectionString'", doc: "The postgres root/superuser connection url for development mode so PG can watch the schema changes, this is strangely named in the postgraphile API 'ownerConnectionString'",
format: String, format: String,
default: "postgresql://postgres:metamigo@127.0.0.1:5435/metamigo_dev", default: "postgresql://postgres:metamigo@127.0.0.1:5435/metamigo_dev",
env: "APP_ROOT_DATABASE_URL", env: "METAMIGO_APP_ROOT_DATABASE_URL",
}, },
authConnection: { authConnection: {
doc: "The postgres connection URL for postgraphile, must not be superuser and must have limited privs.", doc: "The postgres connection URL for postgraphile, must not be superuser and must have limited privs.",
format: String, format: String,
default: default:
"postgresql://metamigo_graphile_auth:metamigo@127.0.0.1:5435/metamigo_dev", "postgresql://metamigo_graphile_auth:metamigo@127.0.0.1:5435/metamigo_dev",
env: "DATABASE_AUTH_URL", env: "METAMIGO_DATABASE_AUTH_URL",
}, },
visitor: { visitor: {
doc: "The postgres role that postgraphile switches to", doc: "The postgres role that postgraphile switches to",
format: String, format: String,
default: "app_postgraphile", default: "app_postgraphile",
env: "DATABASE_VISITOR", env: "METAMIGO_DATABASE_VISITOR",
}, },
schema: { schema: {
doc: "The schema postgraphile should expose with graphql", doc: "The schema postgraphile should expose with graphql",
@ -80,7 +80,7 @@ export const configSchema = {
doc: "Whether to enable the graphiql web interface or not", doc: "Whether to enable the graphiql web interface or not",
format: "Boolean", format: "Boolean",
default: false, default: false,
env: "ENABLE_GRAPHIQL", env: "METAMIGO_ENABLE_GRAPHIQL",
}, },
}, },
@ -89,14 +89,14 @@ export const configSchema = {
doc: "The shadow databse connection url used by postgraphile-migrate. Not needed in production.", doc: "The shadow databse connection url used by postgraphile-migrate. Not needed in production.",
format: "uri", format: "uri",
default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_shadow", default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_shadow",
env: "SHADOW_DATABASE_URL", env: "METAMIGO_SHADOW_DATABASE_URL",
sensitive: true, sensitive: true,
}, },
rootConnection: { rootConnection: {
doc: "The postgres root/superuser connection url for testing only, database must NOT be the app database. Not needed in production.", doc: "The postgres root/superuser connection url for testing only, database must NOT be the app database. Not needed in production.",
format: "uri", format: "uri",
default: "postgresql://postgres:metamigo@127.0.0.1:5435/template1", default: "postgresql://postgres:metamigo@127.0.0.1:5435/template1",
env: "ROOT_DATABASE_URL", env: "METAMIGO_ROOT_DATABASE_URL",
sensitive: true, sensitive: true,
}, },
}, },
@ -105,13 +105,13 @@ export const configSchema = {
doc: "The url the frontend can be accessed at", doc: "The url the frontend can be accessed at",
format: "url", format: "url",
default: "http://localhost:3000", default: "http://localhost:3000",
env: "FRONTEND_URL", env: "METAMIGO_FRONTEND_URL",
}, },
apiUrl: { apiUrl: {
doc: "The url the api backend can be accessed at from the frontend server", doc: "The url the api backend can be accessed at from the frontend server",
format: "url", format: "url",
default: "http://localhost:3001", default: "http://localhost:3001",
env: "API_URL", env: "METAMIGO_API_URL",
}, },
}, },
nextAuth: { nextAuth: {