metamigo: do nextauth v3 -> v4 upgrades
This commit is contained in:
parent
a33f80c497
commit
45f8cb1234
13 changed files with 158 additions and 123 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue