├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── src ├── components │ ├── button.tsx │ └── provider.tsx ├── index.ts └── payload │ ├── endpoint.ts │ ├── finalise.ts │ ├── googleOneTap.ts │ ├── strategy.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | ### Node Patch ### 133 | # Serverless Webpack directories 134 | .webpack/ 135 | 136 | # Optional stylelint cache 137 | 138 | # SvelteKit build / generate output 139 | .svelte-kit 140 | 141 | ### VisualStudioCode ### 142 | .vscode/* 143 | !.vscode/settings.json 144 | !.vscode/tasks.json 145 | !.vscode/launch.json 146 | !.vscode/extensions.json 147 | !.vscode/*.code-snippets 148 | 149 | # Local History for Visual Studio Code 150 | .history/ 151 | 152 | # Built Visual Studio Code Extensions 153 | *.vsix 154 | 155 | ### VisualStudioCode Patch ### 156 | # Ignore all local history of files 157 | .history 158 | .ionide 159 | 160 | # Support for Project snippet scope 161 | .vscode/*.code-snippets 162 | 163 | # Ignore code-workspaces 164 | *.code-workspace 165 | 166 | # End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode 167 | .idea -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | cache 4 | node_modules 5 | package.json 6 | dist 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "auto", 5 | "printWidth": 110, 6 | "semi": false, 7 | "singleQuote": true, 8 | "jsxSingleQuote": true, 9 | "bracketSameLine": false, 10 | "trailingComma": "none", 11 | "tabWidth": 4, 12 | "useTabs": true, 13 | "overrides": [ 14 | { 15 | "files": "package*.json", 16 | "options": { 17 | "printWidth": 1000 18 | } 19 | }, 20 | { 21 | "files": [ 22 | "**/*.css", 23 | "**/*.scss", 24 | "**/*.html", 25 | "**/*.yml" 26 | ], 27 | "options": { 28 | "singleQuote": false 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Scott Adams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # payload-plugin-google-one-tap 2 | 3 | Adds google-one-tap functionality to the login page. 4 | 5 | ## Installation 6 | 7 | ``` 8 | yarn add payload-plugin-google-one-tap 9 | ``` 10 | 11 | You will need to add your google credentials as environment variables: 12 | ```dotenv 13 | PAYLOAD_PUBLIC_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com 14 | GOOGLE_CLIENT_SECRET=your-google-secret 15 | ``` 16 | 17 | Then you can add the plugin to your payload.config: 18 | 19 | ```js 20 | import { googleOneTap } from 'payload-plugin-google-one-tap' 21 | 22 | export default buildConfig({ 23 | ... 24 | plugins: [googleOneTap()], 25 | ... 26 | }) 27 | ``` 28 | 29 | This package uses the following libraries: 30 | 31 | https://github.com/MomenSherif/react-oauth 32 | 33 | https://github.com/Genially/passport-google-one-tap 34 | 35 | You will need to add the following webpack config: 36 | 37 | ```js 38 | admin: { 39 | webpack: (config) => ({ 40 | ...config, 41 | resolve: { 42 | ...config.resolve, 43 | fallback: { 44 | ...config.resolve.fallback, 45 | util: require.resolve('util'), 46 | stream: require.resolve('stream-browserify'), 47 | fs: false, 48 | url: false, 49 | querystring: false, 50 | child_process: false, 51 | assert: false, 52 | tls: false, 53 | net: false, 54 | os: false 55 | } 56 | } 57 | }) 58 | } 59 | ``` 60 | 61 | And finally you will need to add the following to your server.ts file: 62 | ```js 63 | const app = express() 64 | 65 | app.use(express.json()) 66 | ``` 67 | 68 | You can customise the login button if you like by passing in some props in your payload.config.ts. Heres the defaults: 69 | 70 | ```js 71 | plugins: [ 72 | googleOneTap({ 73 | type: 'standard', 74 | theme: 'outline', 75 | size: 'large', 76 | text: 'signin_with', 77 | shape: 'rectangular', 78 | logo_alignment: 'left', 79 | width: undefined, // auto, 80 | locale: undefined, 81 | useOneTap: true, 82 | auto_select: false, 83 | ux_mode: 'popup' 84 | }) 85 | ] 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-plugin-google-one-tap", 3 | "description": "Adds Google One Tap login to Payload CMS", 4 | "version": "1.0.6", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": "Scott Adams ", 8 | "license": "MIT", 9 | "repository": "git@github.com:ScottEAdams/payload-plugin-google-one-tap.git", 10 | "scripts": { 11 | "build": "tsc", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "dependencies": { 18 | "@react-oauth/google": "^0.2.8", 19 | "jsonwebtoken": "^8.5.1", 20 | "passport-google-one-tap": "^1.0.0", 21 | "stream-browserify": "^3.0.0", 22 | "util": "^0.12.5" 23 | }, 24 | "devDependencies": { 25 | "@types/express": "^4.17.14", 26 | "@types/jsonwebtoken": "^8.5.9", 27 | "@types/react-router-dom": "^5.3.3", 28 | "payload": "^1.1.15", 29 | "prettier": "^2.7.1", 30 | "typescript": "^4.6.2" 31 | }, 32 | "peerDependencies": { 33 | "payload": "^1.1.15" 34 | }, 35 | "keywords": [ 36 | "payload", 37 | "plugin", 38 | "google", 39 | "login" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {useHistory} from 'react-router-dom' 3 | import {GoogleLogin} from '@react-oauth/google' 4 | import {useAuth, useConfig} from 'payload/components/utilities' 5 | 6 | export enum GType { 7 | standard = 'standard', 8 | icon = 'icon' 9 | } 10 | 11 | export enum GTheme { 12 | outline = 'outline', 13 | filled_blue = 'filled_blue', 14 | filled_black = 'filled_black' 15 | } 16 | 17 | export enum GSize { 18 | large = 'large', 19 | medium = 'medium', 20 | small = 'small' 21 | } 22 | 23 | export enum GText { 24 | signin_with = 'signin_with', 25 | signup_with = 'signup_with', 26 | continue_with = 'continue_with', 27 | signin = 'signin' 28 | } 29 | 30 | export enum GShape { 31 | rectangular = 'rectangular', 32 | pill = 'pill', 33 | circle = 'circle', 34 | square = 'square' 35 | } 36 | 37 | export enum GAlignment { 38 | left = 'left', 39 | center = 'center' 40 | } 41 | 42 | export enum GUxMode { 43 | popup = 'popup', 44 | redirect = 'redirect' 45 | } 46 | 47 | export interface GoogleOneTapButtonProps { 48 | type?: GType 49 | theme?: GTheme 50 | size?: GSize 51 | text?: GText 52 | shape?: GShape 53 | logo_alignment?: GAlignment 54 | width?: string | undefined 55 | locale?: string | undefined 56 | useOneTap?: boolean 57 | auto_select?: boolean 58 | ux_mode?: GUxMode 59 | } 60 | 61 | interface sucecssJson { 62 | token?: string 63 | } 64 | 65 | const GoogleOneTapButton = (props?: GoogleOneTapButtonProps) => { 66 | return () => { 67 | const { 68 | type = GType.standard, 69 | theme = GTheme.outline, 70 | size = GSize.large, 71 | text = GText.signin_with, 72 | shape = GShape.rectangular, 73 | logo_alignment = GAlignment.left, 74 | width = undefined, 75 | locale = undefined, 76 | useOneTap = true, 77 | auto_select = false, 78 | ux_mode = GUxMode.popup 79 | } = props || {} 80 | const {setToken} = useAuth() 81 | const { 82 | routes: {admin} 83 | } = useConfig() 84 | const history = useHistory() 85 | 86 | const onSuccess = (data: sucecssJson) => { 87 | if (data.token) { 88 | setToken(data.token) 89 | history.push(admin) 90 | } 91 | } 92 | 93 | return ( 94 |
95 | { 108 | try { 109 | const response = await fetch( 110 | `${window.location.protocol}//${window.location.host}/oauth2/callback/google`, 111 | { 112 | method: 'POST', 113 | body: JSON.stringify(credentialResponse), 114 | headers: {'Content-type': 'application/json; charset=UTF-8'} 115 | } 116 | ) 117 | const authed = await response.json() 118 | onSuccess(authed) 119 | } catch (e) { 120 | console.error(e) 121 | } 122 | }} 123 | onError={() => { 124 | console.error('Login Failed') 125 | }} 126 | /> 127 |
128 | ) 129 | } 130 | } 131 | 132 | export default GoogleOneTapButton 133 | -------------------------------------------------------------------------------- /src/components/provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Credit to https://github.com/MomenSherif/react-oauth 3 | */ 4 | import React, { ReactNode } from 'react' 5 | import { GoogleOAuthProvider } from '@react-oauth/google' 6 | 7 | interface Props { 8 | children?: ReactNode 9 | } 10 | 11 | const GoogleOneTapProvider = ({ children }: Props) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | 19 | export default GoogleOneTapProvider 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import googleOneTap from './payload/googleOneTap' 2 | 3 | export { googleOneTap } -------------------------------------------------------------------------------- /src/payload/endpoint.ts: -------------------------------------------------------------------------------- 1 | import finalise from './finalise' 2 | import { PayloadRequest } from 'payload/types' 3 | import { Response } from 'express' 4 | import { Endpoint } from 'payload/config' 5 | 6 | const GoogleOneTapEndpoint: Endpoint = { 7 | path: '/oauth2/callback/google', 8 | method: 'post', 9 | root: true, 10 | handler: async (req: PayloadRequest, res: Response) => { 11 | req.payload.authenticate(req, res, () => { 12 | if (req?.user) { 13 | const final = finalise(req, res) 14 | return final.res.status(200).send({ token: final.token }) 15 | } 16 | return res.status(401).send({ message: 'Unauthorized' }) 17 | }) 18 | } 19 | } 20 | 21 | export default GoogleOneTapEndpoint 22 | -------------------------------------------------------------------------------- /src/payload/finalise.ts: -------------------------------------------------------------------------------- 1 | import { CookieOptions, Response } from 'express' 2 | import jwt from 'jsonwebtoken' 3 | 4 | import { getCookieExpiration, getFieldsToSign, sanitizeInternalFields } from './utils' 5 | import { PayloadRequest } from 'payload/types' 6 | 7 | const finalise = (req: PayloadRequest, res: Response) => { 8 | let user = req.user.toJSON({ virtuals: true }) 9 | user = JSON.parse(JSON.stringify(user)) 10 | user = sanitizeInternalFields(user) 11 | 12 | const collections = req.payload.collections 13 | const userCollection = collections[req.payload.config.admin.user] 14 | const collectionConfig = userCollection.config 15 | 16 | const fieldsToSign = getFieldsToSign(collectionConfig, user) 17 | 18 | const token = jwt.sign(fieldsToSign, req.payload.secret, { 19 | expiresIn: collectionConfig.auth.tokenExpiration 20 | }) 21 | 22 | const cookieOptions: CookieOptions = { 23 | path: '/', 24 | httpOnly: true, 25 | expires: getCookieExpiration(collectionConfig.auth.tokenExpiration), 26 | secure: collectionConfig.auth.cookies.secure, 27 | sameSite: collectionConfig.auth.cookies.sameSite, 28 | domain: undefined 29 | } 30 | 31 | if (collectionConfig.auth.cookies.domain) { 32 | cookieOptions.domain = collectionConfig.auth.cookies.domain 33 | } 34 | 35 | res.cookie(`payload-token`, token, cookieOptions) 36 | 37 | req.user = user 38 | 39 | return { req, res, token } 40 | } 41 | 42 | export default finalise 43 | -------------------------------------------------------------------------------- /src/payload/googleOneTap.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'payload/config' 2 | 3 | import GoogleOneTapButton from '../components/button' 4 | import GoogleOneTapEndpoint from './endpoint' 5 | import GoogleOneTapProvider from '../components/provider' 6 | import GoogleOneTapStrategy from './strategy' 7 | import { addBeforeLogin, addEndpointsToConfig, addProvider, addStrategyToCollection } from './utils' 8 | import { CollectionConfig } from 'payload/types' 9 | 10 | const googleOneTap = 11 | (buttonProps = {}) => 12 | (incomingConfig: Config): Config => { 13 | let config: Config = { 14 | ...incomingConfig 15 | } 16 | 17 | // add the strategy 18 | const userSlug = config?.admin?.user || 'users' 19 | 20 | config.collections = config?.collections?.map((collectionConfig: CollectionConfig) => { 21 | if (collectionConfig.slug === userSlug) { 22 | const strategy = { strategy: GoogleOneTapStrategy, name: 'google-one-tap' } 23 | collectionConfig = addStrategyToCollection(collectionConfig, strategy) 24 | } 25 | return collectionConfig 26 | }) 27 | 28 | // add the endpoint 29 | config = addEndpointsToConfig(config, GoogleOneTapEndpoint) 30 | 31 | // add the components 32 | config = addBeforeLogin(config, GoogleOneTapButton(buttonProps)) 33 | config = addProvider(config, GoogleOneTapProvider) 34 | 35 | return config 36 | } 37 | 38 | export default googleOneTap 39 | -------------------------------------------------------------------------------- /src/payload/strategy.ts: -------------------------------------------------------------------------------- 1 | import { GoogleOneTapStrategy as GOTStrategy } from 'passport-google-one-tap' 2 | import { Payload } from 'payload' 3 | import { Forbidden, LockedAuth } from 'payload/errors' 4 | import { PayloadRequest } from 'payload/types' 5 | 6 | import { makeRandomPassword } from './utils' 7 | 8 | interface GoogleOneTapStrategyOptionWithRequest { 9 | clientID?: string 10 | clientSecret?: string 11 | verifyCsrfToken?: boolean 12 | passReqToCallback: true 13 | } 14 | 15 | const GoogleOneTapStrategy = ({ config, collections }: Payload) => { 16 | const opts: GoogleOneTapStrategyOptionWithRequest = { 17 | clientID: process.env.PAYLOAD_PUBLIC_GOOGLE_CLIENT_ID, 18 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 19 | verifyCsrfToken: false, 20 | passReqToCallback: true 21 | } 22 | 23 | // @ts-ignore 24 | return new GOTStrategy(opts, async (req: PayloadRequest, profile, done) => { 25 | if (req.user) { 26 | done(null, req.user) 27 | } 28 | 29 | const email = profile?.emails?.[0]?.value 30 | if (!email) { 31 | done(Forbidden, false) 32 | } 33 | 34 | try { 35 | const userCollection = collections[config.admin.user] 36 | const collectionConfig = userCollection.config 37 | const slug = collectionConfig.slug 38 | 39 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 40 | // @ts-ignore Improper typing in library, additional args should be optional 41 | let userDoc = await userCollection.Model.findByUsername(email) 42 | 43 | const isLocked = (date: number): boolean => !!(date && date > Date.now()) 44 | if (userDoc && isLocked(userDoc.lockUntil)) { 45 | throw new LockedAuth() 46 | } 47 | 48 | if (!userDoc) { 49 | const password = makeRandomPassword(10) 50 | userDoc = await req.payload.create({ 51 | req, 52 | collection: slug, 53 | data: { email: profile.emails[0].value, password: password, _verified: true } 54 | }) 55 | } 56 | 57 | if (userDoc) { 58 | userDoc.collection = slug 59 | userDoc._strategy = 'google-one-tap' 60 | done(null, userDoc) 61 | } else { 62 | done(null, false) 63 | } 64 | } catch (err) { 65 | done(err, false) 66 | } 67 | }) 68 | } 69 | 70 | export default GoogleOneTapStrategy 71 | -------------------------------------------------------------------------------- /src/payload/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | import { 4 | ArrayField, 5 | BlockField, 6 | CheckboxField, 7 | CodeField, 8 | CollapsibleField, 9 | CollectionConfig, 10 | DateField, 11 | EmailField, 12 | Field, 13 | GroupField, 14 | NumberField, 15 | PointField, 16 | RadioField, 17 | RelationshipField, 18 | RichTextField, 19 | RowField, 20 | SelectField, 21 | TextareaField, 22 | TextField, 23 | UIField, 24 | UploadField 25 | } from 'payload/types' 26 | import { Config, Endpoint } from 'payload/config' 27 | import React from 'react' 28 | 29 | export const makeRandomPassword = (length = 20) => { 30 | const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$' 31 | return Array.from(crypto.randomFillSync(new Uint32Array(length))) 32 | .map((x) => characters[x % characters.length]) 33 | .join('') 34 | } 35 | 36 | export const addStrategyToCollection = (collectionConfig: CollectionConfig, strategy: any) => { 37 | if (!collectionConfig?.auth || typeof collectionConfig.auth === 'boolean') { 38 | collectionConfig.auth = {} 39 | } 40 | const existingStrategies = collectionConfig?.auth?.strategies || [] 41 | collectionConfig.auth.strategies = [...existingStrategies, strategy] 42 | return collectionConfig 43 | } 44 | 45 | export const addEndpointsToConfig = (config: Config, endpoint: Endpoint) => { 46 | const existingEndpoints = config?.endpoints || [] 47 | config.endpoints = [...existingEndpoints, endpoint] 48 | return config 49 | } 50 | 51 | export const addBeforeLogin = (config: Config, component: React.ComponentType) => { 52 | if (!config?.admin) { 53 | config.admin = {} 54 | } 55 | if (!config?.admin?.components) { 56 | config.admin.components = {} 57 | } 58 | const existingComponents = config?.admin?.components?.beforeLogin || [] 59 | config.admin.components.beforeLogin = [...existingComponents, component] 60 | return config 61 | } 62 | 63 | export const addProvider = (config: Config, component: React.ComponentType) => { 64 | if (!config?.admin) { 65 | config.admin = {} 66 | } 67 | if (!config?.admin?.components) { 68 | config.admin.components = {} 69 | } 70 | const existingComponents = config?.admin?.components?.providers || [] 71 | config.admin.components.providers = [...existingComponents, component] 72 | return config 73 | } 74 | 75 | export const getCookieExpiration = (seconds = 7200) => { 76 | const currentTime = new Date() 77 | currentTime.setSeconds(currentTime.getSeconds() + seconds) 78 | return currentTime 79 | } 80 | 81 | const internalFields = ['__v', 'salt', 'hash'] 82 | export const sanitizeInternalFields = (incomingDoc: any) => 83 | Object.entries(incomingDoc).reduce((newDoc, [key, val]) => { 84 | if (key === '_id') { 85 | return { 86 | ...newDoc, 87 | id: val 88 | } 89 | } 90 | 91 | if (internalFields.indexOf(key) > -1) { 92 | return newDoc 93 | } 94 | 95 | return { 96 | ...newDoc, 97 | [key]: val 98 | } 99 | }, {}) 100 | 101 | export const getFieldsToSign = (collectionConfig: any, user: any) => { 102 | type FieldWithSubFields = GroupField | ArrayField | RowField | CollapsibleField 103 | 104 | function fieldHasSubFields(field: Field): field is FieldWithSubFields { 105 | return ( 106 | field.type === 'group' || 107 | field.type === 'array' || 108 | field.type === 'row' || 109 | field.type === 'collapsible' 110 | ) 111 | } 112 | 113 | function fieldIsPresentationalOnly(field: Field): field is UIField { 114 | return field.type === 'ui' 115 | } 116 | 117 | type FieldAffectingData = 118 | | TextField 119 | | NumberField 120 | | EmailField 121 | | TextareaField 122 | | CheckboxField 123 | | DateField 124 | | BlockField 125 | | GroupField 126 | | RadioField 127 | | RelationshipField 128 | | ArrayField 129 | | RichTextField 130 | | SelectField 131 | | UploadField 132 | | CodeField 133 | | PointField 134 | 135 | function fieldAffectsData(field: Field): field is FieldAffectingData { 136 | return 'name' in field && !fieldIsPresentationalOnly(field) 137 | } 138 | 139 | return collectionConfig.fields.reduce( 140 | (signedFields: any, field: Field) => { 141 | const result = { 142 | ...signedFields 143 | } 144 | 145 | if (!fieldAffectsData(field) && fieldHasSubFields(field)) { 146 | field.fields.forEach((subField) => { 147 | if (fieldAffectsData(subField) && subField.saveToJWT) { 148 | result[subField.name] = user[subField.name] 149 | } 150 | }) 151 | } 152 | 153 | if (fieldAffectsData(field) && field.saveToJWT) { 154 | result[field.name] = user[field.name] 155 | } 156 | 157 | return result 158 | }, 159 | { 160 | email: user.email, 161 | id: user.id, 162 | collection: collectionConfig.slug 163 | } 164 | ) 165 | } 166 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "target": "es5", 9 | "outDir": "./dist", 10 | "allowJs": true, 11 | "module": "commonjs", 12 | "sourceMap": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "declaration": true, 16 | "declarationDir": "./dist", 17 | "skipLibCheck": true, 18 | "strict": true 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------