diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..4d301d9 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +.now \ No newline at end of file diff --git a/server/api/gitee.ts b/server/api/gitee.ts new file mode 100644 index 0000000..034c094 --- /dev/null +++ b/server/api/gitee.ts @@ -0,0 +1,26 @@ +import fetch from 'node-fetch' +import { createCodeHandler } from '.' + +const { GITEE_OAUTH_CLIENT_ID, GITEE_OAUTH_CLIENT_SECRET } = process.env + +async function oauth(code: string) { + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: code, + client_id: GITEE_OAUTH_CLIENT_ID, + client_secret: GITEE_OAUTH_CLIENT_SECRET, + redirect_uri: 'https://gitako.now.sh/redirect/', + }) + + const res = await fetch('https://gitee.com/oauth/token?' + params.toString(), { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + redirect: 'follow', + method: 'post', + }) + return res.json() +} + +export default createCodeHandler(oauth) diff --git a/server/api/github.ts b/server/api/github.ts new file mode 100644 index 0000000..a2760ea --- /dev/null +++ b/server/api/github.ts @@ -0,0 +1,31 @@ +import fetch from 'node-fetch' +import { createCodeHandler } from '.' + +const { GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET } = process.env + +async function oauth(code: string) { + const res = await fetch('https://github.com/login/oauth/access_token', { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + redirect: 'follow', + method: 'post', + body: JSON.stringify({ + code, + client_id: GITHUB_OAUTH_CLIENT_ID, + client_secret: GITHUB_OAUTH_CLIENT_SECRET, + }), + }) + + const body = await res.json() + const { access_token: accessToken, scope, error_description: errorDescription } = body + if (errorDescription) { + throw new Error(errorDescription) + } else if (scope !== 'repo' || !accessToken || !(typeof accessToken === 'string')) { + throw new Error(`Cannot resolve response: '${JSON.stringify(res)}'`) + } + return accessToken +} + +export default createCodeHandler(oauth) diff --git a/server/api/index.ts b/server/api/index.ts new file mode 100644 index 0000000..ad16ab6 --- /dev/null +++ b/server/api/index.ts @@ -0,0 +1,36 @@ +import { NowRequest, NowResponse } from '@now/node' + +export function createCodeHandler(oauthHandler: (code: string) => string | Promise) { + return async function handleCode(request: NowRequest, response: NowResponse) { + const { code } = request.query + try { + setCORSHeaders(response) + if (!request.method || request.method.toLowerCase() !== 'post') { + return sendRejection(response, 405) + } + if (!code || typeof code !== 'string') { + return sendRejection(response, 403) + } + const accessToken = await oauthHandler(code) + writeJSON(response, { accessToken }) + response.end() + } catch (err) { + return sendRejection(response, 400, err instanceof Error ? err.message : '') + } + } +} + +function setCORSHeaders(response: NowResponse) { + response.setHeader('Access-Control-Allow-Origin', '*') + response.setHeader('Access-Control-Allow-Methods', 'POST') +} + +export function sendRejection(response: NowResponse, status = 400, content?: string) { + response.writeHead(status) + response.end(content) +} + +function writeJSON(response: NowResponse, data: unknown) { + response.setHeader('Content-Type', 'application/json') + response.write(JSON.stringify(data)) +} diff --git a/server/api/redirect.ts b/server/api/redirect.ts new file mode 100644 index 0000000..3859ce7 --- /dev/null +++ b/server/api/redirect.ts @@ -0,0 +1,21 @@ +import { NowRequest, NowResponse } from '@now/node' +import { sendRejection } from './index' + +export default async function handleRedirect(request: NowRequest, response: NowResponse) { + const { redirect, ...params } = request.query + if (typeof redirect !== 'string' || !redirect) { + return sendRejection(response) + } + + const url = new URL(redirect) + + for (const key of Object.keys(params)) { + const value = params[key] + if (Array.isArray(value)) { + for (const v of value) url.searchParams.append(key, v) + } else url.searchParams.append(key, value) + } + + response.writeHead(307, { Location: url.href }) + response.end() +} diff --git a/server/now.json b/server/now.json new file mode 100644 index 0000000..a7fc7b4 --- /dev/null +++ b/server/now.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "env": { + "GITHUB_OAUTH_CLIENT_SECRET": "@gitako-github-oauth-client-secret", + "GITHUB_OAUTH_CLIENT_ID": "@gitako-github-oauth-client-id", + "GITEE_OAUTH_CLIENT_SECRET": "@gitako-gitee-oauth-client-secret", + "GITEE_OAUTH_CLIENT_ID": "@gitako-gitee-oauth-client-id" + }, + "alias": "gitako.now.sh", + "routes": [ + { "src": "/redirect/?", "dest": "/api/redirect.ts" }, + { "src": "/oauth/github/?", "dest": "/api/github.ts" }, + { "src": "/oauth/gitee/?", "dest": "/api/gitee.ts" } + ] +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..4cd7591 --- /dev/null +++ b/server/package.json @@ -0,0 +1,13 @@ +{ + "name": "gitako-server", + "version": "0.1.0", + "main": "api/index.ts", + "author": "Enix", + "license": "MIT", + "dependencies": { + "@now/node": "^1.5.0", + "@types/node-fetch": "^2.5.5", + "node-fetch": "^2.6.0", + "typescript": "^3.8.3" + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..32fca7f --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,64 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true /* Generates corresponding '.map' file. */, + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./build" /* Redirect output structure to the directory. */, + "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": ["src"] +} diff --git a/server/yarn.lock b/server/yarn.lock new file mode 100644 index 0000000..1330d17 --- /dev/null +++ b/server/yarn.lock @@ -0,0 +1,71 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@now/node@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@now/node/-/node-1.5.0.tgz#a159772276ae11acf87cf2620a86204abff38f1c" + integrity sha512-0MChNIBHsViTMPDsifTveWmMpUSUbVQYAHg9Qa8p7uF7pkfBAghyCGHSKHJVg3zxx0vVE4z+rYgaRS6xvF8qMA== + dependencies: + "@types/node" "*" + +"@types/node-fetch@^2.5.5": + version "2.5.5" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.5.tgz#cd264e20a81f4600a6c52864d38e7fef72485e92" + integrity sha512-IWwjsyYjGw+em3xTvWVQi5MgYKbRs0du57klfTaZkv/B24AEQ/p/IopNeqIYNy3EsfHOpg8ieQSDomPcsYMHpA== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node@*": + version "13.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" + integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-types@^2.1.12: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== diff --git a/src/env.ts b/src/env.ts index 0361686..60fa722 100644 --- a/src/env.ts +++ b/src/env.ts @@ -7,7 +7,6 @@ export const GITHUB_OAUTH = { export const GITEE_OAUTH = { clientId: process.env.GITEE_OAUTH_CLIENT_ID || '', - clientSecret: process.env.GITEE_OAUTH_CLIENT_SECRET || '', } export const VERSION = process.env.VERSION diff --git a/src/platforms/Gitee/index.ts b/src/platforms/Gitee/index.ts index 2ef8d24..13b2d8d 100644 --- a/src/platforms/Gitee/index.ts +++ b/src/platforms/Gitee/index.ts @@ -143,7 +143,9 @@ export const Gitee: Platform = { client_id: GITEE_OAUTH.clientId, scope: 'projects', response_type: 'code', - redirect_uri: window.location.href, + redirect_uri: + 'https://gitako.now.sh/redirect/?' + + new URLSearchParams({ redirect: window.location.href }).toString(), }) return `https://gitee.com/oauth/authorize?` + params.toString() },