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

@ -13,7 +13,7 @@ const minimumProfileSchema = Joi.object()
const minimumUserSchema = Joi.object()
.keys({
id: Joi.string().required(),
userId: Joi.string().required(),
email: Joi.string().email().required(),
})
.unknown(true);

View file

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

View file

@ -1,10 +1,16 @@
/* eslint-disable unicorn/no-null,max-params */
import { createHash, randomBytes } from "node:crypto";
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 { UserId, UnsavedUser, SavedUser } from "../records/user.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
export const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000;
@ -23,7 +29,7 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
private repos: TRepositories,
private readonly sessionMaxAge = defaultSessionMaxAge,
private readonly sessionUpdateAge = defaulteSessionUpdateAge
) { }
) {}
async createUser(profile: UnsavedUser): Promise<SavedUser> {
// @ts-expect-error Typescript doesn't like lodash's omit()
@ -56,12 +62,12 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
return user;
}
async getUserByProviderAccountId(
providerId: string,
async getUserByAccount(
provider: string,
providerAccountId: string
): Promise<SavedUser | null> {
const account = await this.repos.accounts.findBy({
compoundId: getCompoundId(providerId, providerAccountId),
compoundId: getCompoundId(provider, providerAccountId),
});
if (!account) return null;
@ -72,15 +78,16 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
return this.repos.users.update(user);
}
async linkAccount(
userId: string,
providerId: string,
providerType: string,
providerAccountId: string,
refreshToken: string,
accessToken: string,
accessTokenExpires: number
): Promise<void> {
async linkAccount(adapterAccount: AdapterAccount): Promise<void> {
const {
userId,
access_token: accessToken,
refresh_token: refreshToken,
provider: providerId,
providerAccountId,
expires_at: accessTokenExpires,
type: providerType,
} = adapterAccount;
const exists = await this.repos.users.existsById({ id: userId });
if (!exists) return;
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;
if (this.sessionMaxAge) {
const dateExpires = new Date(Date.now() + this.sessionMaxAge);
@ -118,22 +131,41 @@ export class NextAuthAdapter<TRepositories extends IMetamigoRepositories> {
const session: UnsavedSession = {
expires,
userId: user.id,
sessionToken: randomToken(),
userId,
sessionToken,
//sessionToken: randomToken(),
accessToken: randomToken(),
};
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 });
if (!session) return null;
if (session && session.expires && new Date() > session.expires) {
this.repos.sessions.remove(session);
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(

View file

@ -9,20 +9,20 @@ export const configSchema = {
doc: "The postgres connection url.",
format: "uri",
default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_dev",
env: "DATABASE_URL",
env: "METAMIGO_DATABASE_URL",
sensitive: true,
},
name: {
doc: "The name of the postgres database",
format: String,
default: "metamigo_dev",
env: "DATABASE_NAME",
env: "METAMIGO_DATABASE_NAME",
},
owner: {
doc: "The username of the postgres database owner",
format: String,
default: "metamigo",
env: "DATABASE_OWNER",
env: "METAMIGO_DATABASE_OWNER",
},
},
worker: {
@ -30,19 +30,19 @@ export const configSchema = {
doc: "The postgres connection url for the worker database.",
format: "uri",
default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_dev",
env: "WORKER_DATABASE_URL",
env: "METAMIGO_WORKER_DATABASE_URL",
},
concurrency: {
doc: "The number of jobs to run concurrently",
default: 1,
format: "positiveInt",
env: "WORKER_CONCURRENT_JOBS",
env: "METAMIGO_WORKER_CONCURRENT_JOBS",
},
pollInterval: {
doc: "How long to wait between polling for jobs in milliseconds (for jobs scheduled in the future/retries)",
default: 2000,
format: "positiveInt",
env: "WORKER_POLL_INTERVAL_MS",
env: "METAMIGO_WORKER_POLL_INTERVAL_MS",
},
},
postgraphile: {
@ -50,26 +50,26 @@ export const configSchema = {
doc: "The postgres role that postgraphile logs in with",
format: String,
default: "metamigo_graphile_auth",
env: "DATABASE_AUTHENTICATOR",
env: "METAMIGO_DATABASE_AUTHENTICATOR",
},
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'",
format: String,
default: "postgresql://postgres:metamigo@127.0.0.1:5435/metamigo_dev",
env: "APP_ROOT_DATABASE_URL",
env: "METAMIGO_APP_ROOT_DATABASE_URL",
},
authConnection: {
doc: "The postgres connection URL for postgraphile, must not be superuser and must have limited privs.",
format: String,
default:
"postgresql://metamigo_graphile_auth:metamigo@127.0.0.1:5435/metamigo_dev",
env: "DATABASE_AUTH_URL",
env: "METAMIGO_DATABASE_AUTH_URL",
},
visitor: {
doc: "The postgres role that postgraphile switches to",
format: String,
default: "app_postgraphile",
env: "DATABASE_VISITOR",
env: "METAMIGO_DATABASE_VISITOR",
},
schema: {
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",
format: "Boolean",
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.",
format: "uri",
default: "postgresql://metamigo:metamigo@127.0.0.1:5435/metamigo_shadow",
env: "SHADOW_DATABASE_URL",
env: "METAMIGO_SHADOW_DATABASE_URL",
sensitive: true,
},
rootConnection: {
doc: "The postgres root/superuser connection url for testing only, database must NOT be the app database. Not needed in production.",
format: "uri",
default: "postgresql://postgres:metamigo@127.0.0.1:5435/template1",
env: "ROOT_DATABASE_URL",
env: "METAMIGO_ROOT_DATABASE_URL",
sensitive: true,
},
},
@ -105,13 +105,13 @@ export const configSchema = {
doc: "The url the frontend can be accessed at",
format: "url",
default: "http://localhost:3000",
env: "FRONTEND_URL",
env: "METAMIGO_FRONTEND_URL",
},
apiUrl: {
doc: "The url the api backend can be accessed at from the frontend server",
format: "url",
default: "http://localhost:3001",
env: "API_URL",
env: "METAMIGO_API_URL",
},
},
nextAuth: {