Skip to content

Commit

Permalink
Add Google OAuth Support
Browse files Browse the repository at this point in the history
  • Loading branch information
kahlstrm committed Jan 15, 2024
1 parent fb74107 commit 2175b1f
Show file tree
Hide file tree
Showing 13 changed files with 671 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
4 changes: 3 additions & 1 deletion apps/cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand All @@ -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",
Expand Down
50 changes: 50 additions & 0 deletions apps/cms/src/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {}
Expand Down Expand Up @@ -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 ?? "",
},
}),
],
});
2 changes: 1 addition & 1 deletion apps/cms/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react"
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build"],
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions packages/cms-types/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface Config {
}
export interface User {
id: string;
sub?: string | null;
updatedAt: string;
createdAt: string;
enableAPIKey?: boolean | null;
Expand Down
65 changes: 65 additions & 0 deletions packages/cms-types/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Query {

type User {
id: String
sub: String
updatedAt: DateTime
createdAt: DateTime
enableAPIKey: Boolean
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -182,6 +197,7 @@ type usersDocAccess {
}

type UsersDocAccessFields {
sub: UsersDocAccessFields_sub
updatedAt: UsersDocAccessFields_updatedAt
createdAt: UsersDocAccessFields_createdAt
enableAPIKey: UsersDocAccessFields_enableAPIKey
Expand All @@ -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
Expand Down Expand Up @@ -2979,6 +3018,7 @@ type usersAccess {
}

type UsersFields {
sub: UsersFields_sub
updatedAt: UsersFields_updatedAt
createdAt: UsersFields_createdAt
enableAPIKey: UsersFields_enableAPIKey
Expand All @@ -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
Expand Down Expand Up @@ -4763,6 +4826,7 @@ type Mutation {
}

input mutationUserInput {
sub: String
updatedAt: String
createdAt: String
enableAPIKey: Boolean
Expand All @@ -4779,6 +4843,7 @@ input mutationUserInput {
}

input mutationUserUpdateInput {
sub: String
updatedAt: String
createdAt: String
enableAPIKey: Boolean
Expand Down
11 changes: 11 additions & 0 deletions packages/cms/src/plugins/oauth/OAuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Button from "payload/dist/admin/components/elements/Button";

export default function OAuthButton() {
return (
<div style={{ marginBottom: 40 }}>
<Button el="anchor" url="/oauth2/authorize">
Sign in with oAuth
</Button>
</div>
);
}
47 changes: 47 additions & 0 deletions packages/cms/src/plugins/oauth/index.ts
Original file line number Diff line number Diff line change
@@ -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<Config> => {
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);
};
24 changes: 24 additions & 0 deletions packages/cms/src/plugins/oauth/oAuthClient.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 2175b1f

Please sign in to comment.