From 4239e38b763a70ab4252dc8dd84cb3a9c09ce9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahlstr=C3=B6m=20Kalle?= Date: Tue, 21 Nov 2023 19:12:23 +0200 Subject: [PATCH] Add Google OAuth Support --- .env.example | 4 + apps/cms/package.json | 4 +- apps/cms/src/payload.config.ts | 50 ++++ apps/cms/tsconfig.json | 2 +- apps/web/src/middleware.ts | 3 +- packages/cms-types/payload.ts | 1 + packages/cms-types/schema.gql | 65 +++++ .../cms/src/plugins/oauth/OAuthButton.tsx | 11 + packages/cms/src/plugins/oauth/index.ts | 47 ++++ packages/cms/src/plugins/oauth/oAuthClient.ts | 24 ++ packages/cms/src/plugins/oauth/oAuthServer.ts | 243 ++++++++++++++++++ packages/cms/src/plugins/oauth/types.ts | 69 +++++ pnpm-lock.yaml | 151 +++++++++++ 13 files changed, 671 insertions(+), 3 deletions(-) create mode 100644 packages/cms/src/plugins/oauth/OAuthButton.tsx create mode 100644 packages/cms/src/plugins/oauth/index.ts create mode 100644 packages/cms/src/plugins/oauth/oAuthClient.ts create mode 100644 packages/cms/src/plugins/oauth/oAuthServer.ts create mode 100644 packages/cms/src/plugins/oauth/types.ts diff --git a/.env.example b/.env.example index 10cfff24..28aa3eb5 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,7 @@ NEXT_REVALIDATION_KEY="veryprivatekey" PUBLIC_FRONTEND_URL="http://localhost:3000" PUBLIC_SERVER_URL="http://localhost:3001" + +# variables required for Google OAuth 2.0, otherwise disabled +#CLIENT_ID= +#CLIENT_SECRET= diff --git a/apps/cms/package.json b/apps/cms/package.json index 5d26ed96..6670efaf 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -25,7 +25,8 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "lodash": "^4.17.21", - "payload": "^2.8.1" + "payload": "^2.8.1", + "payload-plugin-oauth": "^2.1.1" }, "devDependencies": { "@tietokilta/cms-types": "workspace:*", @@ -34,6 +35,7 @@ "@tietokilta/ui": "workspace:*", "@types/express": "^4.17.21", "@types/lodash": "^4.14.202", + "@types/passport-oauth2": "^1.4.15", "copyfiles": "^2.4.1", "eslint": "^8.56.0", "nodemon": "^2.0.22", diff --git a/apps/cms/src/payload.config.ts b/apps/cms/src/payload.config.ts index fa24bb8c..c9797024 100644 --- a/apps/cms/src/payload.config.ts +++ b/apps/cms/src/payload.config.ts @@ -3,6 +3,7 @@ import { webpackBundler } from "@payloadcms/bundler-webpack"; import { mongooseAdapter } from "@payloadcms/db-mongodb"; import { lexicalEditor } from "@payloadcms/richtext-lexical"; import type { Config } from "@tietokilta/cms-types/payload"; +import { oAuthPlugin } from "payload-plugin-oauth"; import { buildConfig } from "payload/config"; import { Media } from "./collections/media"; import { Pages } from "./collections/pages"; @@ -12,6 +13,9 @@ import { Footer } from "./globals/footer"; import { LandingPage } from "./globals/landing-page"; import { MainNavigation } from "./globals/main-navigation"; +const { CLIENT_ID, CLIENT_SECRET, MONGODB_URI, PUBLIC_FRONTEND_URL } = + process.env; + declare module "payload" { // eslint-disable-next-line @typescript-eslint/no-empty-interface -- not applicable export interface GeneratedTypes extends Config {} @@ -60,4 +64,50 @@ export default buildConfig({ url: process.env.PAYLOAD_DATABASE_URL, }), editor: lexicalEditor({}), + plugins: [ + oAuthPlugin({ + databaseUri: MONGODB_URI ?? "", + clientID: CLIENT_ID ?? "", + clientSecret: CLIENT_SECRET ?? "", + authorizationURL: "https://accounts.google.com/o/oauth2/v2/auth", + tokenURL: "https://www.googleapis.com/oauth2/v4/token", + callbackURL: `${PUBLIC_FRONTEND_URL ?? "http://localhost:3000"}/oauth2/callback`, + scope: ["profile", "email"], + async userinfo(accessToken: string) { + const user = await fetch( + `https://www.googleapis.com/oauth2/v3/userinfo?access_token=${accessToken}`, + ).then((res) => { + if (!res.ok) { + // eslint-disable-next-line no-console -- logging error here is fine + console.error(res); + throw new Error(res.statusText); + } + return res.json() as unknown as { + sub: string; + name: string; + given_name: string; + family_name: string; + email: string; + }; + }); + return { + sub: user.sub, + + // Custom fields to fill in if user is created + name: + user.name || + `${user.given_name} ${user.family_name}` || + "Teemu Teekkari", + email: user.email, + }; + }, + userCollection: Users, + sessionOptions: { + resave: false, + saveUninitialized: false, + // PAYLOAD_SECRET existing is verified in server.ts + secret: process.env.PAYLOAD_SECRET ?? "", + }, + }), + ], }); diff --git a/apps/cms/tsconfig.json b/apps/cms/tsconfig.json index c3788172..ff807501 100644 --- a/apps/cms/tsconfig.json +++ b/apps/cms/tsconfig.json @@ -9,7 +9,7 @@ "skipLibCheck": true, "outDir": "./dist", "rootDir": "./src", - "jsx": "react" + "jsx": "react-jsx" }, "include": ["src"], "exclude": ["node_modules", "dist", "build"], diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 7afd3e85..1179b0af 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -6,7 +6,8 @@ export function middleware(request: NextRequest): NextResponse { if ( pathname.startsWith("/admin") || pathname.startsWith("/media") || - pathname.startsWith("/api") + pathname.startsWith("/api") || + pathname.startsWith("/oauth2") ) { const destination = new URL(process.env.PUBLIC_SERVER_URL || ""); const url = request.nextUrl.clone(); diff --git a/packages/cms-types/payload.ts b/packages/cms-types/payload.ts index 99b3bee1..09e61bf5 100644 --- a/packages/cms-types/payload.ts +++ b/packages/cms-types/payload.ts @@ -63,6 +63,7 @@ export interface Config { } export interface User { id: string; + sub?: string | null; updatedAt: string; createdAt: string; enableAPIKey?: boolean | null; diff --git a/packages/cms-types/schema.gql b/packages/cms-types/schema.gql index 44cc6141..2dc8191a 100644 --- a/packages/cms-types/schema.gql +++ b/packages/cms-types/schema.gql @@ -29,6 +29,7 @@ type Query { type User { id: String + sub: String updatedAt: DateTime createdAt: DateTime enableAPIKey: Boolean @@ -80,6 +81,7 @@ type Users { } input User_where { + sub: User_sub_operator updatedAt: User_updatedAt_operator createdAt: User_createdAt_operator enableAPIKey: User_enableAPIKey_operator @@ -90,6 +92,17 @@ input User_where { OR: [User_where_or] } +input User_sub_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + input User_updatedAt_operator { equals: DateTime not_equals: DateTime @@ -151,6 +164,7 @@ input User_id_operator { } input User_where_and { + sub: User_sub_operator updatedAt: User_updatedAt_operator createdAt: User_createdAt_operator enableAPIKey: User_enableAPIKey_operator @@ -162,6 +176,7 @@ input User_where_and { } input User_where_or { + sub: User_sub_operator updatedAt: User_updatedAt_operator createdAt: User_createdAt_operator enableAPIKey: User_enableAPIKey_operator @@ -182,6 +197,7 @@ type usersDocAccess { } type UsersDocAccessFields { + sub: UsersDocAccessFields_sub updatedAt: UsersDocAccessFields_updatedAt createdAt: UsersDocAccessFields_createdAt enableAPIKey: UsersDocAccessFields_enableAPIKey @@ -190,6 +206,29 @@ type UsersDocAccessFields { password: UsersDocAccessFields_password } +type UsersDocAccessFields_sub { + create: UsersDocAccessFields_sub_Create + read: UsersDocAccessFields_sub_Read + update: UsersDocAccessFields_sub_Update + delete: UsersDocAccessFields_sub_Delete +} + +type UsersDocAccessFields_sub_Create { + permission: Boolean! +} + +type UsersDocAccessFields_sub_Read { + permission: Boolean! +} + +type UsersDocAccessFields_sub_Update { + permission: Boolean! +} + +type UsersDocAccessFields_sub_Delete { + permission: Boolean! +} + type UsersDocAccessFields_updatedAt { create: UsersDocAccessFields_updatedAt_Create read: UsersDocAccessFields_updatedAt_Read @@ -2979,6 +3018,7 @@ type usersAccess { } type UsersFields { + sub: UsersFields_sub updatedAt: UsersFields_updatedAt createdAt: UsersFields_createdAt enableAPIKey: UsersFields_enableAPIKey @@ -2987,6 +3027,29 @@ type UsersFields { password: UsersFields_password } +type UsersFields_sub { + create: UsersFields_sub_Create + read: UsersFields_sub_Read + update: UsersFields_sub_Update + delete: UsersFields_sub_Delete +} + +type UsersFields_sub_Create { + permission: Boolean! +} + +type UsersFields_sub_Read { + permission: Boolean! +} + +type UsersFields_sub_Update { + permission: Boolean! +} + +type UsersFields_sub_Delete { + permission: Boolean! +} + type UsersFields_updatedAt { create: UsersFields_updatedAt_Create read: UsersFields_updatedAt_Read @@ -4763,6 +4826,7 @@ type Mutation { } input mutationUserInput { + sub: String updatedAt: String createdAt: String enableAPIKey: Boolean @@ -4779,6 +4843,7 @@ input mutationUserInput { } input mutationUserUpdateInput { + sub: String updatedAt: String createdAt: String enableAPIKey: Boolean diff --git a/packages/cms/src/plugins/oauth/OAuthButton.tsx b/packages/cms/src/plugins/oauth/OAuthButton.tsx new file mode 100644 index 00000000..2b1d43a4 --- /dev/null +++ b/packages/cms/src/plugins/oauth/OAuthButton.tsx @@ -0,0 +1,11 @@ +import Button from "payload/dist/admin/components/elements/Button"; + +export default function OAuthButton() { + return ( +
+ +
+ ); +} diff --git a/packages/cms/src/plugins/oauth/index.ts b/packages/cms/src/plugins/oauth/index.ts new file mode 100644 index 00000000..79323b7d --- /dev/null +++ b/packages/cms/src/plugins/oauth/index.ts @@ -0,0 +1,47 @@ +import OAuthButton from "./OAuthButton"; + +import { Config } from "payload/config"; +import { TextField } from "payload/types"; + +import type { oAuthPluginOptions } from "./types"; + +export { OAuthButton, oAuthPluginOptions }; + +// Detect client side because some dependencies may be nullified +const CLIENTSIDE = typeof document !== "undefined"; + +export const oAuthPlugin = + (options: oAuthPluginOptions) => + async (incoming: Config): Promise => { + let funcToCall; + if (CLIENTSIDE) { + funcToCall = (await import("./oAuthClient")).default; + } else { + funcToCall = (await import("./oAuthServer")).default; + } + // Shorthands + const collectionSlug = options.userCollection?.slug ?? "users"; + const sub = options.subField?.name ?? "sub"; + + // Spread the existing config + const config: Config = { + ...incoming, + collections: (incoming.collections ?? []).map((c) => { + // Let's track the oAuth id (sub) + if ( + c.slug === collectionSlug && + !c.fields.some((f) => (f as TextField).name === sub) + ) { + c.fields.push({ + name: sub, + type: "text", + admin: { readOnly: true }, + access: { update: () => false }, + }); + } + return c; + }), + }; + + return funcToCall(config, options); + }; diff --git a/packages/cms/src/plugins/oauth/oAuthClient.ts b/packages/cms/src/plugins/oauth/oAuthClient.ts new file mode 100644 index 00000000..79a4ca99 --- /dev/null +++ b/packages/cms/src/plugins/oauth/oAuthClient.ts @@ -0,0 +1,24 @@ +import OAuthButton from "./OAuthButton"; + +import { Config } from "payload/config"; + +import type { oAuthPluginOptions } from "./types"; +function oAuthPluginClient( + incoming: Config, + options: oAuthPluginOptions, +): Config { + const button: React.ComponentType = options.components?.Button ?? OAuthButton; + return { + ...incoming, + admin: { + ...incoming.admin, + components: { + ...incoming.admin?.components, + beforeLogin: (incoming.admin?.components?.beforeLogin ?? []).concat( + button, + ), + }, + }, + }; +} +export default oAuthPluginClient; diff --git a/packages/cms/src/plugins/oauth/oAuthServer.ts b/packages/cms/src/plugins/oauth/oAuthServer.ts new file mode 100644 index 00000000..7d354c05 --- /dev/null +++ b/packages/cms/src/plugins/oauth/oAuthServer.ts @@ -0,0 +1,243 @@ +import session from "express-session"; +import jwt from "jsonwebtoken"; +import passport from "passport"; +import OAuth2Strategy, { VerifyCallback } from "passport-oauth2"; +import { Config } from "payload/config"; +import { PaginatedDocs } from "payload/database"; +import getCookieExpiration from "payload/dist/utilities/getCookieExpiration"; +import { Field, fieldAffectsData, fieldHasSubFields } from "payload/types"; + +import type { oAuthPluginOptions } from "./types"; +import type { Payload } from "payload"; +// very fancy chatGPT code +function genRandomPassword(size: number) { + const charset = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+"; + let password = ""; + const charsetLength = charset.length; + const array = new Uint8Array(size); + + crypto.getRandomValues(array); + + for (let i = 0; i < size; i++) { + password += charset.charAt(array[i] % charsetLength); + } + + return password; +} + +function validateUser(user: unknown): user is User & Record { + return ( + typeof user === "object" && + user !== null && + "email" in user && + typeof user.email === "string" && + "id" in user && + typeof user.id === "string" + ); +} + +let payload: Payload; + +interface User { + email?: string; + id?: string; +} + +function oAuthPluginServer( + incoming: Config, + options: oAuthPluginOptions, +): Config { + // Shorthands + const callbackPath = + options.callbackPath ?? + (options.callbackURL && new URL(options.callbackURL).pathname) ?? + "/oauth2/callback"; + const authorizePath = options.authorizePath ?? "/oauth2/authorize"; + const collectionSlug = options.userCollection?.slug ?? "users"; + const sub = options.subField?.name ?? "sub"; + + // Passport strategy + if (options.clientID) { + const strategy = new OAuth2Strategy( + options, + async ( + accessToken: string, + refreshToken: string, + profile: undefined, + cb: VerifyCallback, + ) => { + let info: { + sub: string; + email: string; + password?: string; + name?: string; + }; + let user: User & { + // dirty fix for the user collection not being defined, but it is fixed with the ? operator + collection?: NonNullable< + oAuthPluginOptions["userCollection"] + >["slug"]; + _strategy?: string; + }; + let users: PaginatedDocs; + try { + // Get the userinfo + info = await options.userinfo?.(accessToken); + if (!info) throw new Error("Failed to get userinfo"); + // Match existing user + users = await payload.find({ + collection: collectionSlug, + where: { [sub]: { equals: info[sub as "sub"] } }, + showHiddenFields: true, + }); + + if (users.docs?.length) { + user = users.docs[0]; + user.collection = collectionSlug; + user._strategy = "oauth2"; + } else { + // Register new user + user = await payload.create({ + collection: collectionSlug, + data: { + ...info, + // Stuff breaks when password is missing + password: info.password ?? genRandomPassword(30), + }, + showHiddenFields: true, + }); + payload.logger.info(`Created user ${user.email}`); + user.collection = collectionSlug; + user._strategy = "oauth2"; + } + + cb(null, user); + } catch (error) { + if (error instanceof Error && "trace" in error) { + // eslint-disable-next-line no-console + console.log("signin.fail", error.message, error.trace); + cb(error); + } + } + }, + ); + + // Alternative? + // strategy.userProfile = async (accessToken, cb) => { + // const user = await options.userinfo?.(accessToken) + // if (!user) cb(new Error('Failed to get userinfo')) + // else cb(null, user) + // } + + passport.use(strategy); + } else { + // eslint-disable-next-line no-console + console.warn("No client id, oauth disabled"); + } + // passport.serializeUser((user: Express.User, done) => { + passport.serializeUser((user: Express.User & User, done) => { + done(null, user.id); + }); + passport.deserializeUser(async (id: string, done) => { + const ok = await payload.findByID({ collection: collectionSlug, id }); + done(null, ok); + }); + + return { + ...incoming, + onInit: async (incomingPayload) => { + if (incoming.onInit) await incoming.onInit(incomingPayload); + // Add additional onInit code by using the onInitExtension function + payload = incomingPayload; + }, + endpoints: (incoming.endpoints ?? []).concat([ + { + path: authorizePath, + method: "get", + root: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + handler: passport.authenticate("oauth2"), + }, + { + path: callbackPath, + method: "get", + root: true, + handler: session(options.sessionOptions), + }, + { + path: callbackPath, + method: "get", + root: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + handler: passport.authenticate("oauth2", { failureRedirect: "/" }), + }, + { + path: callbackPath, + method: "get", + root: true, + handler(req, res) { + // Get the Mongoose user + const collectionConfig = payload.collections[collectionSlug].config; + + // Sanitize the user object + // let user = userDoc.toJSON({ virtuals: true }) + const user = JSON.parse(JSON.stringify(req.user)); + if (!validateUser(user)) { + throw new Error("Invalid user"); + } + // Decide which user fields to include in the JWT + // Why is this done this way? + const fieldsToSign = collectionConfig.fields.reduce( + (signedFields, field: Field) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: Record = { + ...signedFields, + }; + + if (!fieldAffectsData(field) && fieldHasSubFields(field)) { + field.fields.forEach((subField) => { + if (fieldAffectsData(subField) && subField.saveToJWT) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + result[subField.name] = user[subField.name]; + } + }); + } + + if (fieldAffectsData(field) && field.saveToJWT) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + result[field.name] = user[field.name]; + } + + return result; + }, + { + email: user.email, + id: user.id, + collection: collectionConfig.slug, + } as object, + ); + + // Sign the JWT + const token = jwt.sign(fieldsToSign, payload.secret, { + expiresIn: collectionConfig.auth.tokenExpiration, + }); + + // Set cookie + res.cookie(`${payload.config.cookiePrefix}-token`, token, { + path: "/", + httpOnly: true, + expires: getCookieExpiration(collectionConfig.auth.tokenExpiration), + secure: collectionConfig.auth.cookies.secure, + sameSite: collectionConfig.auth.cookies.sameSite, + domain: collectionConfig.auth.cookies.domain || undefined, + }); + + // Redirect to admin dashboard + res.redirect("/admin"); + }, + }, + ]), + }; +} +export default oAuthPluginServer; diff --git a/packages/cms/src/plugins/oauth/types.ts b/packages/cms/src/plugins/oauth/types.ts new file mode 100644 index 00000000..bfb65285 --- /dev/null +++ b/packages/cms/src/plugins/oauth/types.ts @@ -0,0 +1,69 @@ +import Users from "../../collections/Users"; + +import { type SessionOptions } from "express-session"; +import { ComponentType } from "react"; + +import type { Config as PayloadConfig, User } from "../../payload-types"; +import type { StrategyOptions } from "passport-oauth2"; +export interface oAuthPluginOptions extends StrategyOptions { + /** Database connection URI in case the lib needs access to database */ + databaseUri: string; + + /** Options to pass to express-session + * @default + * ```js + * { + * resave: false, + * saveUninitialized: false, + * secret: process.env.PAYLOAD_SECRET, + * store: options.databaseUri + * ? MongoStore.create({ mongoUrl: options.databaseUri }) + * : undefined, + * }), + * ``` + * + */ + sessionOptions: SessionOptions; + + /** Endpoint to handle callback from oauth provider + * Defaults to /oauth/authorize + * Note that this will have /api prepended to it. + * So the default value is actually /api/oauth/authorize + * + * @default /oauth/authorize + */ + authorizePath?: string; + + /** Map an authentication result to a user */ + userinfo: (accessToken: string) => Promise<{ + /** Unique identifier for the linked account */ + sub: string; + /** Unique identifier for the linked account */ + email: string; + /** A password will be generated for new users */ + password?: string; + /** Example of a custom field */ + name?: string; + }>; + + /** Which path to mount in express, defaults to the path in callbackURL */ + callbackPath?: string; + + components?: { + Button?: ComponentType; + }; + userCollection?: typeof Users & { + /** Defaults to "users" */ + slug?: UserCollectionKeys; + }; + /** If the collection does not have a field with name "sub", it will be created */ + subField?: { + /** Defaults to "sub" */ + name?: string; + }; +} +type UserCollectionKeys = { + [K in keyof PayloadConfig["collections"]]: PayloadConfig["collections"][K] extends User + ? K + : never; +}[keyof PayloadConfig["collections"]]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8e41d62..647349c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: payload: specifier: ^2.8.1 version: 2.8.1(typescript@5.3.3)(webpack@5.89.0) + payload-plugin-oauth: + specifier: ^2.1.1 + version: 2.1.1(mongodb@4.17.1)(node-fetch@2.7.0)(passport@0.6.0)(payload@2.8.1)(react@18.2.0) devDependencies: '@tietokilta/cms-types': specifier: workspace:* @@ -66,6 +69,9 @@ importers: '@types/lodash': specifier: ^4.14.202 version: 4.14.202 + '@types/passport-oauth2': + specifier: ^1.4.15 + version: 1.4.15 copyfiles: specifier: ^2.4.1 version: 2.4.1 @@ -1073,6 +1079,14 @@ packages: js-yaml: 4.1.0 dev: false + /@bothrs/util@3.1.3(node-fetch@2.7.0): + resolution: {integrity: sha512-hlSYPa8LO3TvD3E7jYs4+ECwuG/0t0YN0rz1DR1IRqv7GRrdSLHRdLmFmpxkgm+lQuclpwZOKKO+Eu3j9ZIN6g==} + peerDependencies: + node-fetch: 2.x + dependencies: + node-fetch: 2.7.0 + dev: false + /@csstools/cascade-layer-name-parser@1.0.7(@csstools/css-parser-algorithms@2.5.0)(@csstools/css-tokenizer@2.2.3): resolution: {integrity: sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==} engines: {node: ^14 || ^16 || >=18} @@ -4254,10 +4268,30 @@ packages: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: true + /@types/oauth@0.9.4: + resolution: {integrity: sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==} + dependencies: + '@types/node': 20.11.0 + dev: true + /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: false + /@types/passport-oauth2@1.4.15: + resolution: {integrity: sha512-9cUTP/HStNSZmhxXGuRrBJfEWzIEJRub2eyJu3CvkA+8HAMc9W3aKdFhVq+Qz1hi42qn+GvSAnz3zwacDSYWpw==} + dependencies: + '@types/express': 4.17.21 + '@types/oauth': 0.9.4 + '@types/passport': 1.0.16 + dev: true + + /@types/passport@1.0.16: + resolution: {integrity: sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==} + dependencies: + '@types/express': 4.17.21 + dev: true + /@types/prettier@2.7.3: resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} dev: false @@ -4979,6 +5013,15 @@ packages: is-shared-array-buffer: 1.0.2 dev: true + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: false + /ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} dev: true @@ -5070,6 +5113,11 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false + /base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + dev: false + /big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -5091,6 +5139,10 @@ packages: readable-stream: 3.6.2 dev: false + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: false + /body-parser@1.20.1: resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5565,6 +5617,21 @@ packages: engines: {node: '>=0.8'} dev: false + /connect-mongo@4.6.0(express-session@1.17.3)(mongodb@4.17.1): + resolution: {integrity: sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==} + engines: {node: '>=10'} + peerDependencies: + express-session: ^1.17.1 + mongodb: ^4.1.0 + dependencies: + debug: 4.3.4 + express-session: 1.17.3 + kruptein: 3.0.6 + mongodb: 4.17.1 + transitivePeerDependencies: + - supports-color + dev: false + /console-table-printer@2.11.2: resolution: {integrity: sha512-uuUHie0sfPP542TKGzPFal0W1wo1beuKAqIZdaavcONx8OoqdnJRKjkinbRTOta4FaCa1RcIL+7mMJWX3pQGVg==} dependencies: @@ -5595,6 +5662,11 @@ packages: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -7058,6 +7130,22 @@ packages: resolution: {integrity: sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==} dev: false + /express-session@1.17.3: + resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -8479,6 +8567,13 @@ packages: engines: {node: '>= 8'} dev: false + /kruptein@3.0.6: + resolution: {integrity: sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==} + engines: {node: '>8'} + dependencies: + asn1.js: 5.4.1 + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -9189,6 +9284,10 @@ packages: webpack-sources: 1.4.3 dev: false + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -9515,6 +9614,10 @@ packages: boolbase: 1.0.0 dev: false + /oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9795,6 +9898,17 @@ packages: passport-strategy: 1.0.0 dev: false + /passport-oauth2@1.7.0: + resolution: {integrity: sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==} + engines: {node: '>= 0.4.0'} + dependencies: + base64url: 3.0.1 + oauth: 0.9.15 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + dev: false + /passport-strategy@1.0.0: resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} engines: {node: '>= 0.4.0'} @@ -9874,6 +9988,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /payload-plugin-oauth@2.1.1(mongodb@4.17.1)(node-fetch@2.7.0)(passport@0.6.0)(payload@2.8.1)(react@18.2.0): + resolution: {integrity: sha512-SQCcjXj9geh+H1QQRVlEIuHH0FAc9N3U3y2xuw3AViPqUHB0awLSmMCDCUBhUr8csVPGg4CVyroVq4eEcRpa+w==} + peerDependencies: + passport: ^0.6 + payload: ^2 + react: ^18 + dependencies: + '@bothrs/util': 3.1.3(node-fetch@2.7.0) + connect-mongo: 4.6.0(express-session@1.17.3)(mongodb@4.17.1) + debug: 4.3.4 + express-session: 1.17.3 + passport: 0.6.0 + passport-oauth2: 1.7.0 + payload: 2.8.1(typescript@5.3.3)(webpack@5.89.0) + react: 18.2.0 + transitivePeerDependencies: + - mongodb + - node-fetch + - supports-color + dev: false + /payload@2.8.1(typescript@5.3.3)(webpack@5.89.0): resolution: {integrity: sha512-g+FbNWmmx4b/4I9fLxXbx5cKNPCwWJBDTH2SoLqCooAsX5r/NS78Ae5KcWwRH+xVdoiMXG2vQ9Li4npJ+ckJlQ==} engines: {node: '>=14'} @@ -11043,6 +11178,11 @@ packages: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false + /random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -12950,6 +13090,17 @@ packages: resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} + /uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + dependencies: + random-bytes: 1.0.0 + dev: false + + /uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + dev: false + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: