├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── __mocks__ └── @octokit │ └── rest.js ├── commitlint.config.js ├── package-lock.json ├── package.json └── src ├── createPersonalAccessToken.js ├── executables └── ghpat.js ├── prompters ├── basicAuthentication.js ├── basicPermissions.js ├── reason.js └── twoFactorAuthenticationCode.js ├── scopes.js ├── tokenGenerator.js ├── usernameValidator.js └── usernameValidator.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": [ 4 | "@babel/preset-env" 5 | ], 6 | "env": { 7 | "production": { 8 | "presets": [ 9 | "minify" 10 | ] 11 | } 12 | }, 13 | "plugins": [ 14 | "@babel/plugin-transform-runtime" 15 | ], 16 | "ignore": [ 17 | "node_modules", 18 | "*.test.js" 19 | ] 20 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "jest": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintcache 3 | node_modules 4 | npm-debug.log 5 | .travis.yml 6 | src/ 7 | test/ 8 | *.test.js 9 | coverage/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: true 7 | node_js: 8 | - '8' 9 | install: npm install 10 | before_install: 11 | - npm install -g npm@5 12 | - npm install -g greenkeeper-lockfile@1 13 | jobs: 14 | include: 15 | - stage: test 16 | script: 17 | - npm run compile 18 | - npm run lint 19 | - npm run test 20 | before_script: greenkeeper-lockfile-update 21 | after_script: greenkeeper-lockfile-upload 22 | after_success: npm run codecov 23 | - stage: deploy 24 | if: branch = master 25 | script: npm run travis-deploy-once "npm run semantic-release" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jae Bradley 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 | # github-personal-access-token-generator-cli 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/jaebradley/github-personal-access-token-generator-cli.svg)](https://greenkeeper.io/) 4 | [![codecov](https://codecov.io/gh/jaebradley/github-personal-access-token-generator-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/jaebradley/github-personal-access-token-generator-cli) 5 | [![npm](https://img.shields.io/npm/v/github-personal-access-token-generator-cli.svg)](https://www.npmjs.com/package/github-personal-access-token-generator-cli) 6 | [![npm](https://img.shields.io/npm/dt/github-personal-access-token-generator-cli.svg)](https://www.npmjs.com/package/github-personal-access-token-generator-cli) 7 | 8 | ![alt-text](https://imgur.com/NBzcu8N.png) 9 | ![alt-text](https://imgur.com/ZJyz9KJ.png) 10 | 11 | [A GitHub personal access token is pretty seamless to do from the web UI](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/). 12 | 13 | However, if you're a command line freak like me and want to create a personal access token quickly without having to leave the Terminal... 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install github-personal-access-token-generator-cli -g 19 | ``` 20 | 21 | ## Usage 22 | 23 | ``` 24 | ghpat 25 | ``` 26 | 27 | You will be asked for 28 | 29 | * Your username + password combination 30 | * The various scopes / permissions you'd like your access token to have 31 | * A reason for this personal access token - aka what's it going to be used for? 32 | * A two-factor authentication code if you've turned on 2FA for GitHub 33 | 34 | If successful your personal access token **will be copied to your clipboard**. 35 | 36 | ## Uhhh not all the GitHub personal access token options are available? 37 | 38 | Yeah, they're not. 39 | 40 | This utility is mostly to create personal access tokens quickly - most of the time (I would argue), you need to set one of the "overriding" scopes rather than any of the "sub-scopes". 41 | 42 | If this is something that's not the case for end-users, this can be added. 43 | -------------------------------------------------------------------------------- /__mocks__/@octokit/rest.js: -------------------------------------------------------------------------------- 1 | const getForUser = jest.fn(({ username } = {}) => { 2 | if (!username) { 3 | throw new Error(`Unknown username: ${username}`); 4 | } 5 | }); 6 | 7 | const constructor = jest.fn(() => ({ 8 | users: { getForUser }, 9 | })); 10 | 11 | const GitHub = constructor; 12 | 13 | export default GitHub; 14 | export { getForUser }; 15 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-angular'] }; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-personal-access-token-generator-cli", 3 | "version": "0.0.0-development", 4 | "description": "Generate GitHub personal access tokens from the command line", 5 | "main": "index.js", 6 | "bin": { 7 | "ghpat": "build/executables/ghpat.js" 8 | }, 9 | "global": true, 10 | "scripts": { 11 | "codecov": "codecov", 12 | "commitmsg": "commitlint -e $GIT_PARAMS", 13 | "compile": "babel src/ -d build/ --delete-dir-on-start", 14 | "compile:prod": "BABEL_ENV=production npm run compile", 15 | "lint": "eslint --ext .js .", 16 | "test": "jest --no-cache", 17 | "prepublishOnly": "npm run compile:prod", 18 | "semantic-commit": "commit", 19 | "semantic-release": "semantic-release", 20 | "travis-deploy-once": "travis-deploy-once" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/jaebradley/github-personal-access-token-generator-cli.git" 25 | }, 26 | "keywords": [ 27 | "github", 28 | "cli", 29 | "personal access token", 30 | "github personal access token" 31 | ], 32 | "author": "jae.b.bradley@gmail.com", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/jaebradley/github-personal-access-token-generator-cli/issues" 36 | }, 37 | "homepage": "https://github.com/jaebradley/github-personal-access-token-generator-cli#readme", 38 | "jest": { 39 | "testEnvironment": "node", 40 | "collectCoverage": true 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "^7.0.0-beta.49", 44 | "@babel/core": "^7.0.0-beta.49", 45 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.49", 46 | "@babel/plugin-transform-async-to-generator": "^7.0.0-beta.49", 47 | "@babel/plugin-transform-runtime": "^7.0.0-beta.49", 48 | "@babel/preset-env": "^7.0.0-beta.49", 49 | "@babel/runtime": "^7.0.0-beta.49", 50 | "@commitlint/cli": "^7.0.0", 51 | "@commitlint/config-angular": "^7.0.1", 52 | "@commitlint/prompt": "^7.0.0", 53 | "@commitlint/prompt-cli": "^7.0.0", 54 | "babel-core": "^7.0.0-bridge.0", 55 | "babel-jest": "^23.0.0", 56 | "babel-preset-minify": "^0.4.0", 57 | "codecov": "^3.0.2", 58 | "eslint": "^5.0.1", 59 | "eslint-config-airbnb-base": "^13.0.0", 60 | "eslint-plugin-import": "^2.13.0", 61 | "husky": "^0.14.3", 62 | "jest": "^22.0.5", 63 | "semantic-release": "^15.0.0", 64 | "travis-deploy-once": "^5.0.0" 65 | }, 66 | "dependencies": { 67 | "@octokit/rest": "^15.8.1", 68 | "babel-plugin-transform-runtime": "^6.23.0", 69 | "clipboardy": "^1.2.3", 70 | "commander": "^2.12.1", 71 | "inquirer": "^6.0.0", 72 | "inquirer-autocomplete-prompt": "^1.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/createPersonalAccessToken.js: -------------------------------------------------------------------------------- 1 | import { write } from 'clipboardy'; 2 | 3 | import promptReason from './prompters/reason'; 4 | import promptBasicPermissions from './prompters/basicPermissions'; 5 | import { promptBasicAuthentication } from './prompters/basicAuthentication'; 6 | import { promptTwoFactorAuthenticationCode } from './prompters/twoFactorAuthenticationCode'; 7 | 8 | import generateToken from './tokenGenerator'; 9 | 10 | const isTwoFactorAuthenticationError = error => ( 11 | !!error 12 | && !!error.message 13 | && JSON.parse(error.message).message === 'Must specify two-factor authentication OTP code.' 14 | ); 15 | 16 | const createPersonalAccessToken = async () => { 17 | const { username, password } = await promptBasicAuthentication(); 18 | const { scopes } = await promptBasicPermissions(); 19 | const { reason } = await promptReason(); 20 | 21 | try { 22 | const { token } = await generateToken({ 23 | username, 24 | password, 25 | scopes, 26 | reason, 27 | }); 28 | await write(token); 29 | console.log('💯 Copied personal access token to clipboard!'); 30 | } catch (error) { 31 | if (isTwoFactorAuthenticationError(error)) { 32 | const { twoFactorAuthenticationCode } = await promptTwoFactorAuthenticationCode(); 33 | const { token } = await generateToken({ 34 | username, 35 | password, 36 | scopes, 37 | reason, 38 | twoFactorAuthenticationCode, 39 | }); 40 | await write(token); 41 | console.log('💯 Copied personal access token to clipboard!'); 42 | } else { 43 | throw error; 44 | } 45 | } 46 | }; 47 | 48 | export default createPersonalAccessToken; 49 | -------------------------------------------------------------------------------- /src/executables/ghpat.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import program from 'commander'; 4 | 5 | import pkg from '../../package.json'; 6 | import createPersonalAccessToken from '../createPersonalAccessToken'; 7 | 8 | const execute = async () => { 9 | try { 10 | await createPersonalAccessToken(); 11 | } catch (e) { 12 | console.error('😞 Rut ro, an error occurred'); 13 | console.error(e); 14 | } 15 | }; 16 | 17 | program.version(pkg.version) 18 | .description('Create GitHub Personal Access Tokens from the command line') 19 | .parse(process.argv); 20 | 21 | execute(); 22 | -------------------------------------------------------------------------------- /src/prompters/basicAuthentication.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import isValidUsername from '../usernameValidator'; 3 | 4 | 5 | const validateUsername = async (username) => { 6 | const isValid = await isValidUsername(username); 7 | 8 | if (isValid) { 9 | return true; 10 | } 11 | 12 | return `${username} is not a valid username`; 13 | }; 14 | 15 | const validatePassword = (password) => { 16 | if (password && password.length > 0) { 17 | return true; 18 | } 19 | 20 | return 'Please enter a valid password'; 21 | }; 22 | 23 | const promptBasicAuthentication = async () => ( 24 | inquirer.prompt([ 25 | { 26 | name: 'username', 27 | message: 'Input your GitHub Username', 28 | validate: validateUsername, 29 | type: 'input', 30 | }, 31 | { 32 | name: 'password', 33 | message: 'Input your GitHub password', 34 | validate: validatePassword, 35 | type: 'password', 36 | }, 37 | ]) 38 | ); 39 | 40 | export { 41 | validateUsername, 42 | validatePassword, 43 | promptBasicAuthentication, 44 | }; 45 | -------------------------------------------------------------------------------- /src/prompters/basicPermissions.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | import { 4 | REPOSITORY_SCOPE, 5 | ORGANIZATION_SCOPE, 6 | PUBLIC_KEY_SCOPE, 7 | REPOSITORY_HOOK_SCOPE, 8 | GIST_SCOPE, 9 | NOTIFICATIONS_SCOPE, 10 | USER_SCOPE, 11 | DELETE_REPOSITORY_SCOPE, 12 | DISCUSSION_SCOPE, 13 | GPG_KEY_SCOPE, 14 | } from '../scopes'; 15 | 16 | const REPOSITORY_FULL_ACCESS = 'Full control of all repositories'; 17 | const ORGANIZATION_FULL_ACCESS = 'Full control of orgs and teams'; 18 | const PUBLIC_KEY_FULL_ACCESS = 'Full control of user public keys'; 19 | const REPOSITORY_HOOK_FULL_ACCESS = 'Full control of repository hooks'; 20 | const CREATE_GISTS = 'Create gists'; 21 | const ACCESS_NOTIFICATIONS = 'Access notifications'; 22 | const USER_FULL_ACCESS = 'Update all user data'; 23 | const DELETE_REPOSITORIES = 'Delete repositories'; 24 | const DISCUSSIONS_FULL_ACCESS = 'Read and write team discussions'; 25 | const GPG_KEYS_FULL_ACCESS = 'Full control of user gpg keys'; 26 | 27 | const choices = [ 28 | REPOSITORY_FULL_ACCESS, 29 | ORGANIZATION_FULL_ACCESS, 30 | PUBLIC_KEY_FULL_ACCESS, 31 | REPOSITORY_HOOK_FULL_ACCESS, 32 | CREATE_GISTS, 33 | ACCESS_NOTIFICATIONS, 34 | USER_FULL_ACCESS, 35 | DELETE_REPOSITORIES, 36 | DISCUSSIONS_FULL_ACCESS, 37 | GPG_KEYS_FULL_ACCESS, 38 | ]; 39 | 40 | const defaults = [ 41 | REPOSITORY_FULL_ACCESS, 42 | ]; 43 | 44 | const choicesToScopes = Object.freeze({ 45 | [REPOSITORY_FULL_ACCESS]: REPOSITORY_SCOPE.EVERYTHING, 46 | [ORGANIZATION_FULL_ACCESS]: ORGANIZATION_SCOPE.EVERYTHING, 47 | [PUBLIC_KEY_FULL_ACCESS]: PUBLIC_KEY_SCOPE.EVERYTHING, 48 | [REPOSITORY_HOOK_FULL_ACCESS]: REPOSITORY_HOOK_SCOPE.EVERYTHING, 49 | [CREATE_GISTS]: GIST_SCOPE, 50 | [ACCESS_NOTIFICATIONS]: NOTIFICATIONS_SCOPE, 51 | [USER_FULL_ACCESS]: USER_SCOPE.EVERYTHING, 52 | [DELETE_REPOSITORIES]: DELETE_REPOSITORY_SCOPE, 53 | [DISCUSSIONS_FULL_ACCESS]: DISCUSSION_SCOPE.EVERYTHING, 54 | [GPG_KEYS_FULL_ACCESS]: GPG_KEY_SCOPE.EVERYTHING, 55 | }); 56 | 57 | const promptBasicPermissions = async () => { 58 | const { permissions } = await inquirer.prompt([ 59 | { 60 | name: 'permissions', 61 | message: 'Select your personal access token permissions', 62 | type: 'checkbox', 63 | default: defaults, 64 | choices, 65 | }, 66 | ]); 67 | 68 | return { scopes: permissions.map(permission => choicesToScopes[permission]) }; 69 | }; 70 | 71 | export default promptBasicPermissions; 72 | -------------------------------------------------------------------------------- /src/prompters/reason.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | const promptReason = async () => ( 4 | inquirer.prompt([ 5 | { 6 | name: 'reason', 7 | message: 'Reason for Personal Access Token', 8 | type: 'input', 9 | validate: answer => answer && answer.length > 0, 10 | }, 11 | ]) 12 | ); 13 | 14 | export default promptReason; 15 | -------------------------------------------------------------------------------- /src/prompters/twoFactorAuthenticationCode.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | 3 | const validateAuthenticationCode = code => !!code && code.length > 0; 4 | 5 | const promptTwoFactorAuthenticationCode = async () => ( 6 | inquirer.prompt([ 7 | { 8 | name: 'twoFactorAuthenticationCode', 9 | validate: validateAuthenticationCode, 10 | type: 'password', 11 | message: 'Enter your two-factor authentication code', 12 | }, 13 | ]) 14 | ); 15 | 16 | export { validateAuthenticationCode, promptTwoFactorAuthenticationCode }; 17 | -------------------------------------------------------------------------------- /src/scopes.js: -------------------------------------------------------------------------------- 1 | // https://developer.github.com/apps/building-oauth-apps/scopes-for-oauth-apps/#available-scopes 2 | 3 | const READ_ONLY_SCOPE = 'READ_ONLY_SCOPE'; 4 | const REPOSITORY_SCOPE = Object.freeze({ 5 | COMMIT_STATUS: 'REPOSITORY_SCOPE_COMMIT_STATUS', 6 | DEPLOYMENT_STATUS: 'REPOSITORY_SCOPE_DEPLOYMENT_STATUS', 7 | PUBLIC_REPOSITORY: 'REPOSITORY_SCOPE_PUBLIC_REPOSITORY', 8 | INVITE_COLLABORATORS: 'REPOSITORY_SCOPE_INVITE_COLLABORATORS', 9 | EVERYTHING: 'REPOSITORY_SCOPE_FULL_ACCESS', 10 | }); 11 | 12 | const ORGANIZATION_SCOPE = Object.freeze({ 13 | READ_AND_WRITE: 'ORGANIZATION_SCOPE_READ_AND_WRITE_ACCESS', 14 | READ: 'ORGANIZATION_SCOPE_READ_ACCESS', 15 | EVERYTHING: 'ORGANIZATION_SCOPE_FULL_ACCESS', 16 | }); 17 | 18 | const PUBLIC_KEY_SCOPE = Object.freeze({ 19 | WRITE: 'PUBLIC_KEY_SCOPE_WRITE_ACCESS', 20 | READ: 'PUBLIC_KEY_SCOPE_READ_ACCESS', 21 | EVERYTHING: 'PUBLIC_KEY_SCOPE_FULL_ACCESS', 22 | }); 23 | 24 | const REPOSITORY_HOOK_SCOPE = Object.freeze({ 25 | WRITE: 'REPOSITORY_HOOK_SCOPE_WRITE_ACCESS', 26 | READ: 'REPOSITORY_HOOK_SCOPE_READ_ACCESS', 27 | EVERYTHING: 'REPOSITORY_HOOK_SCOPE_FULL_ACCESS', 28 | }); 29 | 30 | const GIST_SCOPE = 'GIST_SCOPE'; 31 | const NOTIFICATIONS_SCOPE = 'NOTIFICATIONS_SCOPE'; 32 | const USER_SCOPE = Object.freeze({ 33 | READ_USER_PROFILE: 'USER_SCOPE_READ_PROFILE_ACCESS', 34 | READ_EMAIL_ADDRESS: 'USER_SCOPE_READ_EMAIL_ADDRESS', 35 | FOLLOW: 'USER_SCOPE_FOLLOW', 36 | EVERYTHING: 'USER_SCOPE_FULL_ACCESS', 37 | }); 38 | 39 | const DELETE_REPOSITORY_SCOPE = 'DELETE_REPOSITORY_SCOPE'; 40 | const DISCUSSION_SCOPE = Object.freeze({ 41 | READ_AND_WRITE: 'DISCUSSION_SCOPE_READ_AND_WRITE_ACCESS', 42 | READ: 'DISCUSSION_SCOPE_READ_ACCESS', 43 | EVERYTHING: 'DISCUSSION_SCOPE_FULL_ACCESS', 44 | }); 45 | 46 | const GPG_KEY_SCOPE = Object.freeze({ 47 | WRITE: 'GPG_KEY_SCOPE_WRITE_ACCESS', 48 | READ: 'GPG_KEY_SCOPE_READ_ACCESS', 49 | EVERYTHING: 'GPG_KEY_SCOPE_FULL_ACCESS', 50 | }); 51 | 52 | const SCOPES = Object.freeze({ 53 | REPOSITORY: REPOSITORY_SCOPE, 54 | ORGANIZATION: ORGANIZATION_SCOPE, 55 | PUBLIC_KEY: PUBLIC_KEY_SCOPE, 56 | REPOSITORY_HOOK: REPOSITORY_HOOK_SCOPE, 57 | GIST: GIST_SCOPE, 58 | NOTIFICATIONS: NOTIFICATIONS_SCOPE, 59 | USER: USER_SCOPE, 60 | DELETE_REPOSITORY: DELETE_REPOSITORY_SCOPE, 61 | DISCUSSION: DISCUSSION_SCOPE, 62 | GPG_KEY: GPG_KEY_SCOPE, 63 | }); 64 | 65 | export { 66 | READ_ONLY_SCOPE, 67 | REPOSITORY_SCOPE, 68 | ORGANIZATION_SCOPE, 69 | PUBLIC_KEY_SCOPE, 70 | REPOSITORY_HOOK_SCOPE, 71 | GIST_SCOPE, 72 | NOTIFICATIONS_SCOPE, 73 | USER_SCOPE, 74 | DELETE_REPOSITORY_SCOPE, 75 | DISCUSSION_SCOPE, 76 | GPG_KEY_SCOPE, 77 | SCOPES, 78 | }; 79 | -------------------------------------------------------------------------------- /src/tokenGenerator.js: -------------------------------------------------------------------------------- 1 | import GitHub from '@octokit/rest'; 2 | import { 3 | READ_ONLY_SCOPE, 4 | REPOSITORY_SCOPE, 5 | ORGANIZATION_SCOPE, 6 | PUBLIC_KEY_SCOPE, 7 | REPOSITORY_HOOK_SCOPE, 8 | GIST_SCOPE, 9 | NOTIFICATIONS_SCOPE, 10 | USER_SCOPE, 11 | DELETE_REPOSITORY_SCOPE, 12 | DISCUSSION_SCOPE, 13 | GPG_KEY_SCOPE, 14 | } from './scopes'; 15 | 16 | const scopeToApiValue = Object.freeze({ 17 | [READ_ONLY_SCOPE]: '', 18 | [REPOSITORY_SCOPE.EVERYTHING]: 'repo', 19 | [ORGANIZATION_SCOPE.EVERYTHING]: 'admin:org', 20 | [PUBLIC_KEY_SCOPE.EVERYTHING]: 'admin:public_key', 21 | [REPOSITORY_HOOK_SCOPE.EVERYTHING]: 'admin:repo_hook', 22 | [GIST_SCOPE]: 'gist', 23 | [NOTIFICATIONS_SCOPE]: 'notifications', 24 | [USER_SCOPE.EVERYTHING]: 'user', 25 | [DELETE_REPOSITORY_SCOPE]: 'delete_repo', 26 | [DISCUSSION_SCOPE.EVERYTHING]: 'write:discussion', 27 | [GPG_KEY_SCOPE.EVERYTHING]: 'admin:gpg_key', 28 | }); 29 | 30 | const generateToken = async ({ 31 | username, 32 | password, 33 | twoFactorAuthenticationCode, 34 | reason, 35 | scopes, 36 | }) => { 37 | const apiValues = scopes.map(scope => scopeToApiValue[scope]); 38 | const client = new GitHub(); 39 | client.authenticate({ 40 | type: 'basic', 41 | username, 42 | password, 43 | }); 44 | 45 | const parameters = { 46 | note: reason, 47 | scopes: apiValues, 48 | }; 49 | 50 | if (twoFactorAuthenticationCode) { 51 | parameters.headers = { 'X-GitHub-OTP': twoFactorAuthenticationCode }; 52 | } 53 | 54 | const response = await client.authorization.create(parameters); 55 | 56 | return { 57 | token: response.data.token, 58 | id: response.data.id, 59 | }; 60 | }; 61 | 62 | export default generateToken; 63 | -------------------------------------------------------------------------------- /src/usernameValidator.js: -------------------------------------------------------------------------------- 1 | import GitHub from '@octokit/rest'; 2 | 3 | const isValidUsername = async (username) => { 4 | try { 5 | await new GitHub().users.getForUser({ username }); 6 | return true; 7 | } catch (e) { 8 | return false; 9 | } 10 | }; 11 | 12 | export default isValidUsername; 13 | -------------------------------------------------------------------------------- /src/usernameValidator.test.js: -------------------------------------------------------------------------------- 1 | import GitHub, { getForUser } from '@octokit/rest'; 2 | import isValidUsername from './usernameValidator'; 3 | 4 | jest.mock('@octokit/rest'); 5 | 6 | describe('usernameValidator', () => { 7 | beforeEach(() => { 8 | GitHub.mockClear(); 9 | getForUser.mockClear(); 10 | }); 11 | 12 | it('successfully validates username', async () => { 13 | expect(await isValidUsername('jaebaebae')).toBe(true); 14 | expect(getForUser).toHaveBeenCalledTimes(1); 15 | expect(getForUser).toHaveBeenCalledWith({ username: 'jaebaebae' }); 16 | }); 17 | 18 | it('fails to validate username', async () => { 19 | expect(await isValidUsername()).toBe(false); 20 | expect(getForUser).toHaveBeenCalledTimes(1); 21 | expect(getForUser).toHaveBeenCalledWith({ username: undefined }); 22 | expect(getForUser).toThrowError('Unknown username: undefined'); 23 | }); 24 | }); 25 | --------------------------------------------------------------------------------