├── .gitignore ├── .vercelignore ├── 404.html ├── LICENSE ├── README.md ├── api ├── auth.ts └── callback.ts ├── index.html ├── lib ├── config.ts └── scopes.ts ├── package.json ├── vercel.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .vercel 4 | .env 5 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Adrián UB 8 | 9 | 50 | 51 | 52 |
53 | 62 | 67 | 76 | 85 | 94 | 103 | 112 | 121 | 130 | 139 | 145 | 151 | 157 | 158 | 169 | 170 | 179 | 180 | 185 | 191 | 197 | 203 | 204 | 209 | 210 | 214 | 223 | 234 | 245 | 253 | 262 | 268 | 279 | 280 | 281 | 289 | 290 | 301 | 302 | 303 | 312 | 313 | 314 | 323 | 324 | 325 | 334 | 335 | 336 | 345 | 346 | 347 | 356 | 357 | 358 | 369 | 370 | 371 | 383 | 384 | 385 | 394 | 395 | 401 | 402 | 407 | 408 | 413 | 414 | 420 | 421 | 430 | 431 | 437 | 438 | 447 | 448 | 454 | 455 | 464 | 465 | 466 | 475 | 476 | 477 | 486 | 487 | 493 | 494 | 503 | 504 | 510 | 511 | 520 | 521 | 527 | 528 | 537 | 538 | 539 | 548 | 549 | 555 | 556 | 565 | 566 | 572 | 578 | 579 | 588 | 589 | 590 | 599 | 600 | 601 | 610 | 611 | 620 | 626 | 627 | 636 | 637 | 643 | 644 | 653 | 654 | 660 | 661 | 670 | 671 | 672 | 681 | 682 | 688 | 689 | 698 | 699 | 700 | 709 | 710 | 711 | 720 | 721 | 722 | 731 | 732 | 733 | 738 | 739 | 744 | 745 | 751 | 757 | 763 | 764 | 773 | 774 | 782 | 790 | 791 | 800 | 801 | 807 | 808 | 817 | 818 | 824 | 825 | 826 | 827 | 833 | 838 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 856 | 861 | 867 | 868 | 869 | 870 | 871 | 875 | 876 | 877 | 878 |
879 | 880 | 881 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adrián UB 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 | Deploy with Vercel 2 | 3 |
4 |

5 | Netlify CMS on Vercel 6 |

7 |

8 | A simple OAuth2 serverless gateway for Netlify CMS 9 |

10 |
11 | 12 | --- 13 | 14 | ## Why do I need this? 15 | 16 | If you would like to use Netlify CMS to manage your site deployed to Vercel. 17 | 18 | [GitHub](https://github.com) and [Gitlab](https://gitlab.com) requires a server for authentication and Netlify provides this server only for sites deployed to it. Fortunately, such server is rather small and can work with Vercel's serverless functions. 19 | 20 | ## Usage 21 | 22 | In yours projects modify `config.yml` file: 23 | 24 | ```yaml 25 | backend: 26 | name: [github | gitlab] 27 | repo: adrian-ub/adrian-ub # Path to your Github/Gitlab repository 28 | branch: main # Branch to update 29 | base_url: https://netlify-cms.adrianub.vercel.app 30 | ``` 31 | 32 | ## Deploy 33 | 34 | - Create Github OAuth App: 35 | - Go to [developer settings](https://github.com/settings/developers) 36 | - Set `Authorization callback URL` to your deployed oauth website's callback URL: 37 | `https://netlify-cms.adrianub.vercel.app/callback` 38 | - Create Gitlab OAuth app: 39 | - Go to [User settings > Applications](https://gitlab.com/-/profile/applications) 40 | - Set `Redirect URI` to your deployed oauth website's callback URL: 41 | `https://netlify-cms.adrianub.vercel.app/callback` 42 | - Set environment variables on `Vercel` 43 | 44 | ```shell 45 | OAUTH_GITHUB_CLIENT_ID= 46 | OAUTH_GITHUB_CLIENT_SECRET= 47 | 48 | OAUTH_GITLAB_CLIENT_ID= 49 | OAUTH_GITLAB_CLIENT_SECRET= 50 | ``` 51 | 52 | ## Authors 53 | 54 | - Adrián UB ([@AdrianUB](https://twitter.com/AdrianUB)) 55 | -------------------------------------------------------------------------------- /api/auth.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http"; 2 | import { AuthorizationCode } from "simple-oauth2"; 3 | import { randomBytes } from "crypto"; 4 | import { config, Provider } from "../lib/config"; 5 | import { scopes } from "../lib/scopes"; 6 | 7 | export const randomString = () => randomBytes(4).toString("hex"); 8 | 9 | export default async (req: IncomingMessage, res: ServerResponse) => { 10 | const { host } = req.headers; 11 | const url = new URL(`https://${host}/${req.url}`); 12 | const urlParams = url.searchParams; 13 | const provider = urlParams.get("provider") as Provider; 14 | 15 | const client = new AuthorizationCode(config(provider)); 16 | 17 | const authorizationUri = client.authorizeURL({ 18 | redirect_uri: `https://${host}/callback?provider=${provider}`, 19 | scope: scopes[provider], 20 | state: randomString(), 21 | }); 22 | 23 | res.writeHead(301, { Location: authorizationUri }); 24 | res.end(); 25 | }; 26 | -------------------------------------------------------------------------------- /api/callback.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http"; 2 | import { AuthorizationCode } from "simple-oauth2"; 3 | import { config, Provider } from "../lib/config"; 4 | 5 | export default async (req: IncomingMessage, res: ServerResponse) => { 6 | const { host } = req.headers; 7 | const url = new URL(`https://${host}/${req.url}`); 8 | const urlParams = url.searchParams; 9 | const code = urlParams.get("code"); 10 | const provider = urlParams.get("provider") as Provider; 11 | 12 | try { 13 | if (!code) throw new Error(`Missing code ${code}`); 14 | 15 | const client = new AuthorizationCode(config(provider)); 16 | const tokenParams = { 17 | code, 18 | redirect_uri: `https://${host}/callback?provider=${provider}`, 19 | }; 20 | 21 | const accessToken = await client.getToken(tokenParams); 22 | const token = accessToken.token["access_token"] as string; 23 | 24 | const responseBody = renderBody("success", { 25 | token, 26 | provider, 27 | }); 28 | 29 | res.statusCode = 200; 30 | res.end(responseBody); 31 | } catch (e) { 32 | res.statusCode = 200; 33 | res.end(renderBody("error", e)); 34 | } 35 | }; 36 | 37 | function renderBody( 38 | status: string, 39 | content: { 40 | token: string; 41 | provider: string; 42 | } 43 | ) { 44 | return ` 45 | 60 | `; 61 | } 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Netlify CMS on Vercel 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 22 | 23 | 27 | 31 | 35 | 36 | 56 | 116 | 117 | 118 | 119 | 120 | 125 | 151 | 152 | 153 |
154 |
157 |
160 |
161 | 168 | Netlify icon 169 | 172 | 173 | + 174 | 181 | Vercel icon 182 | 183 | 184 |
185 | 186 | Use Netlify CMS for sites hosted on Vercel. 187 |
188 |
189 |
190 | 191 |
192 |
195 |
196 |

197 | Usage 198 |

199 |

200 | In yours projects modify 201 | config.yml 204 | file: 205 |

206 |
207 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | backend: 218 |
219 |
220 |
221 | name: 222 | [ 223 | github | gitlab 224 | ] 225 |
226 |
227 | repo: 228 | adrian-ub/adrian-ub 229 | # Path to your Github/Gitlab 230 |
231 |
232 | repository branch: 233 | main 234 | # Branch to update 235 |
236 |
237 | base_url: 238 | 239 | https://netlify-cms.adrianub.vercel.app 240 | 241 |
242 |
243 |
244 |
245 |
246 | 247 | 268 |
269 |
270 | 279 | 284 | 293 | 302 | 311 | 320 | 334 | 346 | 358 | 367 | 376 | 385 | 394 | 403 | 412 | 421 | 433 | 445 | 457 | 469 | 481 | 493 | 505 | 514 | 523 | 532 | 544 | 556 | 568 | 580 | 592 | 593 | 605 | 606 | 607 | 616 | 617 | 626 | 635 | 644 | 653 | 662 | 671 | 680 | 689 | 698 | 707 | 716 | 725 | 734 | 743 | 752 | 761 | 770 | 779 | 788 | 797 | 806 | 807 | 819 | 820 | 821 | 833 | 834 | 835 | 847 | 848 | 849 | 861 | 862 | 863 | 868 | 873 | 885 | 886 | 887 | 892 | 893 | 905 | 906 | 907 | 911 | 912 | 924 | 925 | 926 | 927 | 936 | 945 | 954 | 963 | 972 | 981 | 990 | 999 | 1008 | 1017 | 1026 | 1035 | 1044 | 1053 | 1062 | 1063 | 1071 | 1080 | 1081 | 1082 | 1083 | 1088 | 1097 | 1098 | 1107 | 1108 | 1117 | 1118 | 1128 | 1129 | 1138 | 1147 | 1156 | 1165 | 1177 | 1189 | 1190 | 1195 | 1196 | 1205 | 1206 | 1207 | 1216 | 1217 | 1226 | 1235 | 1244 | 1253 | 1254 | 1266 | 1267 | 1276 | 1285 | 1286 | 1298 | 1299 | 1300 | 1312 | 1313 | 1314 | 1319 | 1331 | 1343 | 1352 | 1353 | 1361 | 1370 | 1371 | 1372 | 1381 | 1390 | 1391 | 1396 | 1405 | 1406 | 1415 | 1416 | 1425 | 1434 | 1443 | 1452 | 1461 | 1470 | 1479 | 1488 | 1497 | 1498 | 1510 | 1511 | 1512 | 1517 | 1518 | 1519 | 1524 | 1525 | 1526 | 1535 | 1536 | 1548 | 1560 | 1561 | 1566 | 1575 | 1584 | 1593 | 1602 | 1603 | 1612 | 1613 | 1622 | 1631 | 1632 | 1641 | 1642 | 1651 | 1660 | 1669 | 1678 | 1687 | 1696 | 1697 | 1702 | 1703 | 1704 | 1709 | 1710 | 1719 | 1728 | 1737 | 1738 | 1747 | 1748 | 1757 | 1766 | 1775 | 1784 | 1793 | 1802 | 1811 | 1820 | 1829 | 1838 | 1847 | 1848 | 1858 | 1859 | 1868 | 1877 | 1886 | 1895 | 1904 | 1905 | 1914 | 1915 | 1924 | 1933 | 1942 | 1951 | 1960 | 1969 | 1978 | 1987 | 1996 | 2005 | 2014 | 2015 | 2025 | 2026 | 2035 | 2036 | 2044 | 2053 | 2054 | 2055 | 2064 | 2073 | 2082 | 2091 | 2100 | 2101 | 2102 | 2103 | 2109 | 2114 | 2120 | 2121 | 2122 | 2123 | 2124 | 2125 | 2126 | 2132 | 2137 | 2143 | 2144 | 2145 | 2146 | 2147 | 2151 | 2152 | 2153 | 2154 |
2155 |
2156 |
2157 | 2158 | 2198 | 2199 | 2200 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | export type Provider = "github" | "gitlab"; 2 | export const providers: Provider[] = ["github", "gitlab"]; 3 | 4 | export const config = (provider: Provider) => { 5 | if (!providers.includes(provider)) { 6 | throw new Error(`Unsupported provider ${provider}`); 7 | } 8 | return { 9 | client: client[provider], 10 | auth: auth[provider], 11 | }; 12 | }; 13 | 14 | const auth: Record< 15 | Provider, 16 | { tokenHost: string; tokenPath: string; authorizePath: string } 17 | > = { 18 | github: { 19 | tokenHost: "https://github.com", 20 | tokenPath: "/login/oauth/access_token", 21 | authorizePath: "/login/oauth/authorize", 22 | }, 23 | gitlab: { 24 | tokenHost: "https://gitlab.com", 25 | tokenPath: "/oauth/token", 26 | authorizePath: "/oauth/authorize", 27 | }, 28 | }; 29 | 30 | const client: Record = { 31 | github: { 32 | id: process.env.OAUTH_GITHUB_CLIENT_ID as string, 33 | secret: process.env.OAUTH_GITHUB_CLIENT_SECRET as string, 34 | }, 35 | gitlab: { 36 | id: process.env.OAUTH_GITLAB_CLIENT_ID as string, 37 | secret: process.env.OAUTH_GITLAB_CLIENT_SECRET as string, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/scopes.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "./config"; 2 | 3 | export const scopes: Record = { 4 | github: "repo,user", 5 | gitlab: "api", 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adrianub/cms", 3 | "author": "Adrián UB", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "simple-oauth2": "~5.0.0" 9 | }, 10 | "devDependencies": { 11 | "@types/node": "^18.14.0", 12 | "@types/simple-oauth2": "^5.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/auth", "destination": "/api/auth.ts" }, 4 | { "source": "/callback", "destination": "/api/callback.ts" } 5 | ], 6 | "github": { 7 | "silent": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@hapi/boom@^10.0.1": 6 | "integrity" "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==" 7 | "resolved" "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz" 8 | "version" "10.0.1" 9 | dependencies: 10 | "@hapi/hoek" "^11.0.2" 11 | 12 | "@hapi/bourne@^3.0.0": 13 | "integrity" "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" 14 | "resolved" "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz" 15 | "version" "3.0.0" 16 | 17 | "@hapi/hoek@^10.0.1": 18 | "integrity" "sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==" 19 | "resolved" "https://registry.npmjs.org/@hapi/hoek/-/hoek-10.0.1.tgz" 20 | "version" "10.0.1" 21 | 22 | "@hapi/hoek@^11.0.2": 23 | "integrity" "sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==" 24 | "resolved" "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.2.tgz" 25 | "version" "11.0.2" 26 | 27 | "@hapi/hoek@^9.0.0": 28 | "integrity" "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" 29 | "resolved" "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz" 30 | "version" "9.3.0" 31 | 32 | "@hapi/topo@^5.0.0": 33 | "integrity" "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==" 34 | "resolved" "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz" 35 | "version" "5.1.0" 36 | dependencies: 37 | "@hapi/hoek" "^9.0.0" 38 | 39 | "@hapi/wreck@^18.0.0": 40 | "integrity" "sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==" 41 | "resolved" "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.0.1.tgz" 42 | "version" "18.0.1" 43 | dependencies: 44 | "@hapi/boom" "^10.0.1" 45 | "@hapi/bourne" "^3.0.0" 46 | "@hapi/hoek" "^11.0.2" 47 | 48 | "@sideway/address@^4.1.3": 49 | "integrity" "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==" 50 | "resolved" "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz" 51 | "version" "4.1.4" 52 | dependencies: 53 | "@hapi/hoek" "^9.0.0" 54 | 55 | "@sideway/formula@^3.0.1": 56 | "integrity" "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" 57 | "resolved" "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz" 58 | "version" "3.0.1" 59 | 60 | "@sideway/pinpoint@^2.0.0": 61 | "integrity" "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" 62 | "resolved" "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" 63 | "version" "2.0.0" 64 | 65 | "@types/node@^18.14.0": 66 | "integrity" "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==" 67 | "resolved" "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz" 68 | "version" "18.14.0" 69 | 70 | "@types/simple-oauth2@^5.0.2": 71 | "integrity" "sha512-GoqbWy5SmiwGfBfASHcuzHqkBz/fzr0iPrlO41MqQfpGzjLzaqQDkjgghC4YgK1+SGX3pPx24urnlw89xqJ/eA==" 72 | "resolved" "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-5.0.2.tgz" 73 | "version" "5.0.2" 74 | 75 | "debug@^4.3.4": 76 | "integrity" "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==" 77 | "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" 78 | "version" "4.3.4" 79 | dependencies: 80 | "ms" "2.1.2" 81 | 82 | "joi@^17.6.4": 83 | "integrity" "sha512-q5Fn6Tj/jR8PfrLrx4fpGH4v9qM6o+vDUfD4/3vxxyg34OmKcNqYZ1qn2mpLza96S8tL0p0rIw2gOZX+/cTg9w==" 84 | "resolved" "https://registry.npmjs.org/joi/-/joi-17.8.3.tgz" 85 | "version" "17.8.3" 86 | dependencies: 87 | "@hapi/hoek" "^9.0.0" 88 | "@hapi/topo" "^5.0.0" 89 | "@sideway/address" "^4.1.3" 90 | "@sideway/formula" "^3.0.1" 91 | "@sideway/pinpoint" "^2.0.0" 92 | 93 | "ms@2.1.2": 94 | "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 95 | "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" 96 | "version" "2.1.2" 97 | 98 | "simple-oauth2@~5.0.0": 99 | "integrity" "sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==" 100 | "resolved" "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.0.0.tgz" 101 | "version" "5.0.0" 102 | dependencies: 103 | "@hapi/hoek" "^10.0.1" 104 | "@hapi/wreck" "^18.0.0" 105 | "debug" "^4.3.4" 106 | "joi" "^17.6.4" 107 | --------------------------------------------------------------------------------