├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── ParseRest.js └── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib 40 | .idea 41 | 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parse OAuth2 SNS (Social Media) 2 | 3 | [![npm version](https://badge.fury.io/js/parse-oauth2-sns.svg)](https://badge.fury.io/js/parse-oauth2-sns) 4 | 5 | > Node.JS & Express module for 6 | > social media (Facebook, Google, Instagram) auth and login to [parse-server](https://github.com/ParsePlatform/parse-server). 7 | > Plus, Korean SNS (Social Media) supports (Naver, Daum, Kakao) 8 | 9 | ## Install 10 | 11 | ``` 12 | npm install --save parse-oauth2-sns 13 | ``` 14 | 15 | np 16 | How to Use 17 | 18 | --- 19 | 20 | ### For Application 21 | 22 | 1. Use internal browser (like Android Webview) 23 | 24 | 2. Open auth url : /facebook/auth 25 | 26 | 27 | ```javascript 28 | http://__your_host__/oauth2/facebook/auth 29 | ``` 30 | 31 | 3. Check url changed to '/callback' 32 | 33 | 4. Then url chenged to '/callback', get authdata from body. 34 | 35 | 36 | ```javascript 37 | // URL : facebook/callback 38 | {"access_token":"...","expiration_date":"..."} 39 | ``` 40 | 41 | ### For Web 42 | 43 | 1. Open auth url with URL in callback parameter : /facebook/auth?callback=URL 44 | 45 | 46 | ```javascript 47 | window.location.href = 48 | "http://__your_host__/oauth2/facebook/auth?callback=" + 49 | encodeURIComponent("/loginCallback?type=facebook"); 50 | ``` 51 | 52 | | Params | Type | Description | 53 | | -------- | ------ | :------------------------------------------------------------------ | 54 | | callback | string | callback url. Redirected after authentication | 55 | | host | string | If using proxy, can change api url host. ex) host=**your_host**/api | 56 | 57 | 2. Then URL is called, get authdata from querystring. 58 | 59 | 60 | ```javascript 61 | http://__host__/loginCallback?type=facebook&access_token=...& expiration_date=... 62 | ``` 63 | 64 | ## Routes 65 | 66 | ### Facebook Routes 67 | 68 | * /facebook/auth 69 | 70 | * request [get] : callback (url, option), host (url, option) 71 | 72 | * response : redirect to Facebook OAuth page 73 | 74 | * /facebook/callback 75 | 76 | * request : from facebook OAuth page 77 | 78 | * response : json 79 | 80 | ```javascript 81 | {"access_token":"...","expiration_date":"..."} 82 | ``` 83 | 84 | * /facebook/login 85 | 86 | * request [post] : json (facebook auth info) 87 | 88 | ```javascript 89 | {"access_token":"...","expiration_date":"..."} 90 | ``` 91 | 92 | * response : parse-serve user object (username equal to facebook email) 93 | 94 | ```javascript 95 | {"objectId": "ziJdB2jBul", "username": "__facebook.email__", authData, ...} 96 | ``` 97 | 98 | ### Google Routes 99 | 100 | * /google/auth 101 | 102 | * request [get] : callback (url, option), host (url, option) 103 | 104 | * response : redirect to Google OAuth page 105 | 106 | * /google/callback 107 | 108 | * request : from google OAuth page 109 | 110 | * response : json 111 | 112 | ```javascript 113 | {"access_token":"...","expiration_date":"..."} 114 | ``` 115 | 116 | * /google/login 117 | 118 | * request [post] : json (google auth info) 119 | 120 | ```javascript 121 | {"access_token":"...","expiration_date":"..."} 122 | ``` 123 | 124 | * response : parse-serve user object (username equal to google email) 125 | 126 | ```javascript 127 | {"objectId": "ziJdB2jBul", "username": "__google.email__", authData, ...} 128 | ``` 129 | 130 | ### Instagram Routes 131 | 132 | * /instagram/auth 133 | 134 | * request [get] : callback (url, option), host (url, option) 135 | 136 | * response : redirect to Instagram OAuth page 137 | 138 | * /instagram/callback 139 | 140 | * request : from instagram OAuth page 141 | 142 | * response : json 143 | 144 | ```javascript 145 | {"access_token":"...","user":"..."} 146 | ``` 147 | 148 | * /instagram/login 149 | 150 | * request [post] : json (instagram auth info) 151 | 152 | ```javascript 153 | {"access_token":"..."} 154 | ``` 155 | 156 | * response : parse-server user object (username equal to instagram username) 157 | 158 | ```javascript 159 | {"objectId": "ziJdB2jBul", "username": "__instagram.username__", authData, ...} 160 | ``` 161 | 162 | * /instagram/link : parse-server user link to instagram user. 163 | 164 | * request [post] : instagram token and parse-server user info. 165 | 166 | ```javascript 167 | {"access_token":"", "username": "__parse-server user.username__"} 168 | ``` 169 | 170 | * response : parse-server user object linked instagram 171 | 172 | ```javascript 173 | {"objectId": "ziJdB2jBul", "username": "__username__", authData, ...} 174 | ``` 175 | 176 | * /instagram/recent : get recent post from instagram 177 | 178 | * request [get] : userId (parse-server user.objectId) 179 | 180 | * response : instagram posts 181 | 182 | ```javascript 183 | [{images, caption, comments, ...}, ...] 184 | ``` 185 | 186 | ### Naver Routes 187 | 188 | * /naver/auth 189 | 190 | * request [get] : callback (url, option), host (url, option) 191 | 192 | * response : redirect to naver OAuth page 193 | 194 | * /naver/callback 195 | 196 | * request : from naver OAuth page 197 | 198 | * response : json 199 | 200 | ```javascript 201 | {"access_token":"...","expiration_date":"..."} 202 | ``` 203 | 204 | * /naver/login 205 | 206 | * request [post] : json (naver auth info) 207 | 208 | ```javascript 209 | {"access_token":"...","expiration_date":"..."} 210 | ``` 211 | 212 | * response : parse-serve user object (username equal to naver email) 213 | 214 | ```javascript 215 | {"objectId": "ziJdB2jBul", "username": "__naver.email__", authData, ...} 216 | ``` 217 | 218 | ### Daum Routes 219 | 220 | * /daum/auth 221 | 222 | * request [get] : callback (url, option), host (url, option) 223 | 224 | * response : redirect to daum OAuth page 225 | 226 | * /daum/callback 227 | 228 | * request : from daum OAuth page 229 | 230 | * response : json 231 | 232 | ```javascript 233 | {"access_token":"...","expiration_date":"..."} 234 | ``` 235 | 236 | * /daum/login 237 | 238 | * request [post] : json (daum auth info) 239 | 240 | ```javascript 241 | {"access_token":"...","expiration_date":"..."} 242 | ``` 243 | 244 | * response : parse-server user object (username equal to daum userid, not email provided) 245 | 246 | ```javascript 247 | {"objectId": "ziJdB2jBul", "username": "__daum.userid__", authData, ...} 248 | ``` 249 | 250 | ### Kako Routes 251 | 252 | * /kakao/auth 253 | 254 | * request [get] : callback (url, option), host (url, option) 255 | 256 | * response : redirect to kakao OAuth page 257 | 258 | * /kakao/callback 259 | 260 | * request : from kakao OAuth page 261 | 262 | * response : json 263 | 264 | ```javascript 265 | {"access_token":"...","expiration_date":"..."} 266 | ``` 267 | 268 | * /kakao/login 269 | 270 | * request [post] : json (kakao auth info) 271 | 272 | ```javascript 273 | {"access_token":"...","expiration_date":"..."} 274 | ``` 275 | 276 | * response : parse-server user object (username equal to kakao email or kakao userid) 277 | 278 | ```javascript 279 | {"objectId": "ziJdB2jBul", "username": "__kakao.(kaccount_email||id)__", authData, ...} 280 | ``` 281 | 282 | ## Initialize 283 | 284 | ### Setup up process.env 285 | 286 | * It's work with [parse-rest-nodejs](https://github.com/gimdongwoo/parse-oauth2-sns). 287 | 288 | ```javascript 289 | // Recommend to use 'better-npm-run'. 290 | process.env.SERVER_URL = "http://__host__:__port__/parse"; 291 | process.env.APP_ID = "__app_id__"; 292 | process.env.MASTER_KEY = "__master_key__"; 293 | process.env.FB_APPIDS = "__fb_key__"; 294 | process.env.FB_SECRETS = "__fb_secret__"; 295 | process.env.GOOGLE_APPIDS = "__google_key__"; 296 | process.env.GOOGLE_SECRETS = "__goole_secret__"; 297 | process.env.INSTA_APPIDS = "__insta_key__"; 298 | process.env.INSTA_SECRETS = "__insta_secret__"; 299 | process.env.NAVER_APPIDS = "__naver_key__"; 300 | process.env.NAVER_SECRETS = "__naver_secret__"; 301 | process.env.DAUM_APPIDS = "__daum_key__"; 302 | process.env.DAUM_SECRETS = "__daum_secret__"; 303 | process.env.KAKAO_RESTKEY = "__kakao_restkey__"; 304 | process.env.KAKAO_SECRETS = "__kakao_secret__"; 305 | ``` 306 | 307 | ### Router using Express 308 | 309 | * load module 310 | 311 | ```javascript 312 | // es6 313 | import express from "express"; 314 | import session from "express-session"; 315 | import SocialOAuth2 from "parse-oauth2-sns"; 316 | import bodyParser from "body-parser"; 317 | ``` 318 | 319 | ```javascript 320 | // es5 321 | var express = require("express"); 322 | var session = require("session"); 323 | var SocialOAuth2 = require("parse-oauth2-sns").default; 324 | var bodyParser = require("body-parser"); 325 | ``` 326 | 327 | * create object 328 | 329 | ```javascript 330 | // for use req.session 331 | app.use( 332 | session({ 333 | secret: "___secret_key_for_session___", 334 | resave: false, 335 | saveUninitialized: false 336 | // cookie: { maxAge: 60000 } 337 | }) 338 | ); 339 | // for use req.body 340 | app.use(bodyParser.json()); 341 | 342 | // OAuth2 343 | app.use("/oauth2", SocialOAuth2.create({ path: "/oauth2" })); 344 | ``` 345 | 346 | ```javascript 347 | // OR OAuth2 + userObject Handler 348 | // Handler is normal function or promise function. 349 | app.use('/oauth2', SocialOAuth2.create({ path: '/oauth2', userHandler: function(req, user) { ... return user; } })); 350 | ``` 351 | 352 | * Full code is in [test.js](https://github.com/gimdongwoo/parse-oauth2-sns/blob/master/test.js) 353 | 354 | ## Addon Features 355 | 356 | ### User 357 | 358 | * user block/ban 359 | 360 | * if user.isBanned value is setted, user can't login. 361 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-oauth2-sns", 3 | "version": "1.3.3", 4 | "description": "Parse-server module for implementing an OAuth2 Social Media Login with Express in Node.js", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --presets=babel-preset-es2015,babel-preset-stage-0 --out-dir lib", 8 | "prepublish": "npm run build", 9 | "dev-server": "mongodb-runner start && node test.js", 10 | "dev": "npm run build && npm run dev-server" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/gimdongwoo/parse-oauth2-sns.git" 15 | }, 16 | "keywords": [ 17 | "parse-server", 18 | "oauth2", 19 | "nodejs", 20 | "express", 21 | "facebook", 22 | "google", 23 | "instagram", 24 | "social-login", 25 | "sns", 26 | "naver", 27 | "daum" 28 | ], 29 | "author": "Dongwoo Gim (https://github.com/gimdongwoo)", 30 | "license": "Apache-2.0", 31 | "bugs": { 32 | "url": "https://github.com/gimdongwoo/parse-oauth2-sns/issues" 33 | }, 34 | "homepage": "https://github.com/gimdongwoo/parse-oauth2-sns#readme", 35 | "devDependencies": { 36 | "babel-cli": "^6.22.2", 37 | "babel-preset-es2015": "^6.22.0", 38 | "babel-preset-stage-0": "^6.22.0", 39 | "mongodb-runner": "^4.0.0", 40 | "parse-server": "^2.8.4" 41 | }, 42 | "dependencies": { 43 | "body-parser": "^1.18.3", 44 | "express": "^4.16.3", 45 | "express-session": "^1.15.1", 46 | "oauth": "^0.9.15", 47 | "request": "^2.88.0", 48 | "path": "^0.12.7", 49 | "querystring": "^0.2.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ParseRest.js: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import qs from 'querystring'; 3 | 4 | const methods = ['get', 'post', 'put', 'patch', 'del']; 5 | const DEFAULT_TIMEOUT = 15000; 6 | 7 | function makeHeaders(headers, req) { 8 | const _header = { 9 | 'X-Parse-Application-Id': process.env.APP_ID || 'myAppId', 10 | 'Content-Type': 'application/json' 11 | }; 12 | 13 | if (headers && headers.useMasterKey) { 14 | delete headers.useMasterKey; 15 | _header['X-Parse-Master-Key'] = process.env.MASTER_KEY || 'myMasterKey'; 16 | } 17 | 18 | const _headers = Object.assign({}, _header, headers || {}); 19 | 20 | // // sessionToken from session 21 | // const { session = {} } = req; 22 | // // req header first 23 | // const _sessionToken = req.headers.sessiontoken || req.headers.sessionToken || session.sessiontoken || session.sessionToken || (session.user && (session.user.sessiontoken || session.user.sessionToken)); 24 | // if (_sessionToken) { 25 | // _headers['X-Parse-Session-Token'] = _sessionToken; 26 | // } 27 | 28 | return _headers; 29 | } 30 | 31 | function handleRequestError(reject, error, body) { 32 | if (error && error.code === 'ETIMEDOUT') { 33 | return reject({ code: 124, error: 'Request timeout' }); 34 | } 35 | 36 | if (typeof reject === 'function') { 37 | reject(error || body); 38 | } 39 | } 40 | 41 | function qsStringify(str) { 42 | const oldEscape = qs.escape; 43 | qs.escape = function (q) { return q; }; 44 | const stringified = qs.stringify(str); 45 | qs.escape = oldEscape; 46 | return stringified; 47 | } 48 | 49 | function makeUrl(url, data) { 50 | if (!data) return url; 51 | 52 | const query = JSON.parse(JSON.stringify(data)); // deep clone object 53 | 54 | // default order 55 | if (!query.objectId && !query.order) query.order = '-createdAt'; 56 | 57 | // if the user wants to add 'include' or 'key' (or other types of) constraints while getting only one object 58 | // objectId can be added to the query object and is deleted after it's appended to the url 59 | if (query.objectId) { 60 | url += '/' + query.objectId; 61 | delete query.objectId; 62 | } 63 | 64 | // check to see if there is a 'where' object in the query object 65 | // the 'where' object need to be stringified by JSON.stringify(), not querystring 66 | if (typeof query.where === 'object' && Object.keys(query.where).length) { 67 | url += '?where=' + encodeURIComponent(JSON.stringify(query.where)); 68 | } 69 | delete query.where; 70 | 71 | // if there are no more constraints left in the query object 'remainingQuery' will be an empty string 72 | const remainingQuery = qsStringify(query); 73 | if (remainingQuery) { 74 | url += (url.indexOf('?') === -1 ? '?' : '&') + remainingQuery; 75 | } 76 | 77 | return url; 78 | } 79 | 80 | function restCall(method, url, data, _headers, formData) { 81 | const requestUrl = url.indexOf('://') > -1 ? url : process.env.SERVER_URL + url; 82 | 83 | const requestParams = { 84 | timeout: DEFAULT_TIMEOUT, 85 | headers: _headers, 86 | url: requestUrl 87 | }; 88 | 89 | // form, formData 90 | if (data && !data.fileData) { 91 | switch (method) { 92 | case 'patch': 93 | case 'post': 94 | case 'put': 95 | requestParams.body = JSON.stringify(data); // file option 96 | break; 97 | default: 98 | requestParams.url = makeUrl(requestParams.url, data); 99 | break; 100 | } 101 | } 102 | 103 | // add formData 104 | if (formData) requestParams.formData = formData; 105 | 106 | // log 107 | if (process.env.NODE_ENV !== 'production') { 108 | console.log('method :', method); 109 | console.log('requestParams :', requestParams); 110 | } 111 | 112 | // file stream 113 | if (data && data.fileData) { 114 | requestParams.body = data.fileData.file; 115 | requestParams.headers['Content-Type'] = data.fileData.mimetype || 'text/plain'; 116 | } 117 | 118 | return new Promise((resolve, reject) => { 119 | request[method](requestParams, (error, response, body) => { 120 | try { 121 | body = ((typeof body === 'string' && body) ? JSON.parse(body) : body); 122 | error = ((typeof error === 'string' && error) ? JSON.parse(error) : error); 123 | 124 | if (typeof body === 'object') { 125 | const _keys = Object.keys(body); 126 | if (_keys.length === 1 && (_keys[0] === 'results' || _keys[0] === 'result')) { 127 | body = body[_keys[0]]; 128 | } 129 | } 130 | } catch (err) { 131 | console.error('JSON.parse error : ', err); 132 | } finally { 133 | // return 200: success, 204: no content 134 | // xhr.status >= 200 && xhr.status < 300 135 | if (!error && response.statusCode >= 200 && response.statusCode < 300) { 136 | resolve(body); 137 | } else { 138 | console.error('apiCall error : ', error, body); 139 | handleRequestError(reject, error, body); 140 | } 141 | } 142 | }); 143 | }); 144 | } 145 | 146 | /** 147 | * api call 148 | * @method ['get', 'post', 'put', 'patch', 'del'] 149 | * @param (String) url 150 | * @param (Object) data 151 | * @param (Object) headers 152 | * @param (Object) formData 153 | */ 154 | export default class ParseRest { 155 | constructor(req) { 156 | methods.forEach((method) => { 157 | this[method] = (url, data, headers, formData) => { 158 | return restCall(method, url, data, makeHeaders(headers, req), formData); 159 | }; 160 | }); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // error code 2 | // 101: ObjectNotFound 3 | // 102: InvalidQuery 4 | 5 | import { Router } from "express"; 6 | import qs from "querystring"; 7 | import { OAuth2 } from "oauth"; 8 | import path from "path"; 9 | import ParseRest from "./ParseRest"; 10 | 11 | function qsStringify(str) { 12 | const oldEscape = qs.escape; 13 | qs.escape = function(q) { 14 | return q; 15 | }; 16 | const stringified = qs.stringify(str); 17 | qs.escape = oldEscape; 18 | return stringified; 19 | } 20 | 21 | // keep user info to session = default 22 | function defaultUserHandler(req, _user) { 23 | // error 24 | if (!_user) return {}; 25 | 26 | // login results 27 | if (typeof req.session === "object") { 28 | if (_user.sessionToken) req.session.sessionToken = _user.sessionToken; 29 | 30 | req.session.user = _user; 31 | req.session.user.sessionToken = req.session.sessionToken; 32 | 33 | return req.session.user; 34 | } 35 | return _user; 36 | } 37 | 38 | function keyConverter(_envKey) { 39 | let returnKey = _envKey; 40 | 41 | if (typeof _envKey === "object" && Array.isArray(_envKey)) { 42 | [returnKey] = _envKey; 43 | } else if (typeof _envKey === "string" && _envKey.indexOf("[") > -1) { 44 | [returnKey] = JSON.parse(_envKey); 45 | } 46 | 47 | return returnKey; 48 | } 49 | 50 | function fbOAuth2() { 51 | const appId = keyConverter(process.env.FB_APPIDS); 52 | const secret = keyConverter(process.env.FB_SECRETS); 53 | return new OAuth2( 54 | appId, 55 | secret, 56 | "", 57 | "https://www.facebook.com/dialog/oauth", 58 | "https://graph.facebook.com/oauth/access_token", 59 | null 60 | ); 61 | } 62 | 63 | function googleOAuth2() { 64 | const appId = keyConverter(process.env.GOOGLE_APPIDS); 65 | const secret = keyConverter(process.env.GOOGLE_SECRETS); 66 | return new OAuth2( 67 | appId, 68 | secret, 69 | "", 70 | "https://accounts.google.com/o/oauth2/v2/auth", 71 | "https://www.googleapis.com/oauth2/v4/token", 72 | null 73 | ); 74 | } 75 | 76 | function instaOAuth2() { 77 | const appId = keyConverter(process.env.INSTA_APPIDS); 78 | const secret = keyConverter(process.env.INSTA_SECRETS); 79 | return new OAuth2( 80 | appId, 81 | secret, 82 | "", 83 | "https://api.instagram.com/oauth/authorize/", 84 | "https://api.instagram.com/oauth/access_token", 85 | null 86 | ); 87 | } 88 | 89 | function naverOAuth2() { 90 | const appId = keyConverter(process.env.NAVER_APPIDS); 91 | const secret = keyConverter(process.env.NAVER_SECRETS); 92 | return new OAuth2( 93 | appId, 94 | secret, 95 | "", 96 | "https://nid.naver.com/oauth2.0/authorize", 97 | "https://nid.naver.com/oauth2.0/token", 98 | null 99 | ); 100 | } 101 | 102 | function daumOAuth2() { 103 | const appId = keyConverter(process.env.DAUM_APPIDS); 104 | const secret = keyConverter(process.env.DAUM_SECRETS); 105 | return new OAuth2( 106 | appId, 107 | secret, 108 | "", 109 | "https://apis.daum.net/oauth2/authorize", 110 | "https://apis.daum.net/oauth2/token", 111 | null 112 | ); 113 | } 114 | 115 | function kakaoOAuth2() { 116 | const appId = keyConverter(process.env.KAKAO_RESTKEY); 117 | const secret = keyConverter(process.env.KAKAO_SECRETS); 118 | return new OAuth2( 119 | appId, 120 | secret, 121 | "", 122 | "https://kauth.kakao.com/oauth/authorize", 123 | "https://kauth.kakao.com/oauth/token", 124 | null 125 | ); 126 | } 127 | 128 | function makeRedirectUri(req, uri) { 129 | let _host = req.get("host"); 130 | 131 | // query keep to session store 132 | const { callback, host } = req.query; 133 | if (typeof req.session === "object" && (callback || host)) { 134 | req.session.oauth2 = { callback, host }; 135 | } 136 | 137 | // host from session store 138 | if (req.session.oauth2 && req.session.oauth2.host) 139 | _host = req.session.oauth2.host; 140 | 141 | // redirect_uri 142 | const redirectUri = 143 | (req.headers["x-forwarded-proto"] === "https" || req.secure 144 | ? "https" 145 | : "http") + 146 | "://" + 147 | path.join(_host, uri); 148 | return redirectUri; 149 | } 150 | 151 | function callbackResult(req, res, authData) { 152 | if ( 153 | typeof req.session === "object" && 154 | req.session.oauth2 && 155 | req.session.oauth2.callback 156 | ) { 157 | const { callback } = req.session.oauth2; 158 | const joint = callback.indexOf("?") > -1 ? "&" : "?"; 159 | return res.redirect(callback + joint + qsStringify(authData)); 160 | } 161 | return res.json(authData); 162 | } 163 | 164 | export default class SocialOAuth2 { 165 | /** 166 | * @param {Object?} api - Express router 167 | * @return {Object} express router 168 | */ 169 | static create(options, api = Router()) { 170 | const router = new SocialOAuth2(options); 171 | 172 | // facebook 173 | api.get("/facebook/auth", (req, res) => router.facebookAuth(req, res)); 174 | api.get("/facebook/callback", (req, res) => 175 | router.facebookCallback(req, res) 176 | ); 177 | api.post("/facebook/login", (req, res) => router.facebookLogin(req, res)); 178 | 179 | // google 180 | api.get("/google/auth", (req, res) => router.googleAuth(req, res)); 181 | api.get("/google/callback", (req, res) => router.googleCallback(req, res)); 182 | api.post("/google/login", (req, res) => router.googleLogin(req, res)); 183 | 184 | // instagram 185 | api.get("/instagram/auth", (req, res) => router.instagramAuth(req, res)); 186 | api.get("/instagram/callback", (req, res) => 187 | router.instagramCallback(req, res) 188 | ); 189 | api.post("/instagram/login", (req, res) => router.instagramLogin(req, res)); 190 | api.post("/instagram/link", (req, res) => router.instagramLink(req, res)); 191 | api.get("/instagram/recent", (req, res) => 192 | router.instagramRecent(req, res) 193 | ); 194 | 195 | // naver 196 | api.get("/naver/auth", (req, res) => router.naverAuth(req, res)); 197 | api.get("/naver/callback", (req, res) => router.naverCallback(req, res)); 198 | api.post("/naver/login", (req, res) => router.naverLogin(req, res)); 199 | 200 | // daum 201 | api.get("/daum/auth", (req, res) => router.daumAuth(req, res)); 202 | api.get("/daum/callback", (req, res) => router.daumCallback(req, res)); 203 | api.post("/daum/login", (req, res) => router.daumLogin(req, res)); 204 | 205 | // kakao 206 | api.get("/kakao/auth", (req, res) => router.kakaoAuth(req, res)); 207 | api.get("/kakao/callback", (req, res) => router.kakaoCallback(req, res)); 208 | api.post("/kakao/login", (req, res) => router.kakaoLogin(req, res)); 209 | 210 | return api; 211 | } 212 | 213 | constructor(options) { 214 | const _path = options.path; 215 | 216 | // facebook 217 | this.fbOAuth2 = fbOAuth2(); 218 | this.fbRedirectUri = path.join(_path, "/facebook/callback"); 219 | 220 | // google 221 | this.googleOAuth2 = googleOAuth2(); 222 | this.googleRedirectUri = path.join(_path, "/google/callback"); 223 | 224 | // instagram 225 | this.instaOAuth2 = instaOAuth2(); 226 | this.instaRedirectUri = path.join(_path, "/instagram/callback"); 227 | 228 | // naver 229 | this.naverOAuth2 = naverOAuth2(); 230 | this.naverRedirectUri = path.join(_path, "/naver/callback"); 231 | 232 | // daum 233 | this.daumOAuth2 = daumOAuth2(); 234 | this.daumRedirectUri = path.join(_path, "/daum/callback"); 235 | 236 | // kakao 237 | this.kakaoOAuth2 = kakaoOAuth2(); 238 | this.kakaoRedirectUri = path.join(_path, "/kakao/callback"); 239 | 240 | // userHandler 241 | const _userHandler = options.userHandler || defaultUserHandler; 242 | 243 | this.userHandler = (_req, user) => { 244 | return new Promise(resolve => { 245 | if (typeof _userHandler.then == "function") { 246 | _userHandler(_req, user).then(resolve); 247 | } else { 248 | setTimeout(() => resolve(_userHandler(_req, user)), 0); 249 | } 250 | }); 251 | }; 252 | } 253 | 254 | // 255 | // facebook 256 | // 257 | facebookAuth(req, res) { 258 | // For eg. "http://localhost:3000/facebook/callback" 259 | const params = { 260 | redirect_uri: makeRedirectUri(req, this.fbRedirectUri), 261 | scope: "email,public_profile" 262 | }; 263 | console.log("params", params); 264 | return res.redirect(this.fbOAuth2.getAuthorizeUrl(params)); 265 | } 266 | 267 | facebookCallback(req, res) { 268 | if (req.error_reason) { 269 | res.send(req.error_reason); 270 | } 271 | if (req.query && req.query.code) { 272 | // For eg. "/facebook/callback" 273 | this.fbOAuth2.getOAuthAccessToken( 274 | req.query.code, 275 | { 276 | grant_type: "authorization_code", 277 | redirect_uri: makeRedirectUri(req, this.fbRedirectUri) 278 | }, 279 | (err, accessToken, refreshToken, params) => { 280 | if (err) { 281 | console.error(err); 282 | return res.send(err); 283 | } 284 | 285 | const facebookAuth = { 286 | access_token: accessToken, 287 | expiration_date: params.expires 288 | }; 289 | // when custom callback 290 | return callbackResult(req, res, facebookAuth); 291 | } 292 | ); 293 | } 294 | } 295 | 296 | /** 297 | * @param {String} accessToken 298 | * @return {Object} parse user 299 | */ 300 | facebookLogin(req, res) { 301 | const { body = {}, session = {} } = req; 302 | console.log("body", body); 303 | console.log("session", session); 304 | const accessToken = body.access_token || session.access_token; 305 | const expires = body.expiration_date || session.expiration_date; 306 | if (!accessToken) 307 | return res 308 | .status(500) 309 | .json({ code: 101, error: "Invalid facebook access_token" }) 310 | .end(); 311 | 312 | function errorFn(err) { 313 | console.error(err); 314 | return res 315 | .status(500) 316 | .json(err) 317 | .end(); 318 | } 319 | 320 | // https://developers.facebook.com/docs/graph-api/reference/v2.2/user 321 | this.fbOAuth2.get( 322 | "https://graph.facebook.com/me?fields=id,name,email", 323 | accessToken, 324 | (err, data /* , response */) => { 325 | if (err) { 326 | return errorFn(err); 327 | } 328 | 329 | const profile = JSON.parse(data); 330 | console.log(profile); 331 | const profileImageUrl = 332 | "https://graph.facebook.com/" + profile.id + "/picture"; 333 | 334 | const authData = { 335 | facebook: { 336 | id: profile.id, 337 | access_token: accessToken, 338 | expiration_date: expires 339 | } 340 | }; 341 | 342 | if (!profile.email) 343 | return errorFn({ code: 101, error: "Email is unknown" }); 344 | 345 | const parseRest = new ParseRest(req); 346 | parseRest 347 | .get( 348 | "/users", 349 | { where: { username: profile.email } }, 350 | { useMasterKey: true } 351 | ) 352 | .then(users => { 353 | if (users && users[0]) { 354 | // Retrieving 355 | const user = users[0]; 356 | // ban user 357 | if (user.isBanned) 358 | return errorFn({ code: 101, error: "User is banned" }); 359 | // save param 360 | const _param = { socialType: "facebook", authData }; 361 | parseRest 362 | .put("/users/" + user.objectId, _param, { useMasterKey: true }) 363 | .then(() => { 364 | // session query 365 | parseRest 366 | .get( 367 | "/sessions", 368 | { 369 | where: { 370 | user: { 371 | __type: "Pointer", 372 | className: "_User", 373 | objectId: user.objectId 374 | } 375 | } 376 | }, 377 | { useMasterKey: true } 378 | ) 379 | .then(sessions => { 380 | if (sessions && sessions[0]) { 381 | const _session = sessions[0]; 382 | if (typeof req.session === "object") 383 | req.session.sessionToken = _session.sessionToken; 384 | // end 385 | return this.userHandler(req, { 386 | ...user, 387 | ..._param, 388 | sessionToken: _session.sessionToken 389 | }).then(handledUser => res.json(handledUser)); 390 | } 391 | // login 392 | const password = 393 | typeof profile.id === "number" 394 | ? profile.id.toString() 395 | : profile.id; 396 | return parseRest 397 | .put( 398 | "/users/" + user.objectId, 399 | { password }, 400 | { useMasterKey: true } 401 | ) 402 | .then(() => { 403 | return parseRest 404 | .get("/login", { 405 | username: profile.email, 406 | password 407 | }) 408 | .then(result => { 409 | // reload 410 | parseRest 411 | .get("/users/me", null, { 412 | "X-Parse-Session-Token": result.sessionToken 413 | }) 414 | .then(_user => { 415 | // end 416 | return this.userHandler(req, { 417 | ..._user, 418 | sessionToken: result.sessionToken 419 | }).then(handledUser => res.json(handledUser)); 420 | }, errorFn); 421 | }, errorFn); 422 | }, errorFn); 423 | }, errorFn); 424 | }, errorFn); 425 | } else { 426 | // New 427 | const user = { 428 | username: profile.email, 429 | password: 430 | typeof profile.id === "number" 431 | ? profile.id.toString() 432 | : profile.id, 433 | name: profile.name, 434 | email: profile.email, 435 | socialType: "facebook", 436 | socialProfile: profile, 437 | profileImage: { url: profileImageUrl }, 438 | authData 439 | }; 440 | parseRest 441 | .post("/users", user, { useMasterKey: true }) 442 | .then(result => { 443 | // reload 444 | parseRest 445 | .get("/users/me", null, { 446 | "X-Parse-Session-Token": result.sessionToken 447 | }) 448 | .then(_user => { 449 | // end 450 | return this.userHandler(req, { 451 | ..._user, 452 | sessionToken: result.sessionToken 453 | }).then(handledUser => res.json(handledUser)); 454 | }, errorFn); 455 | }, errorFn); 456 | } 457 | }, errorFn); 458 | } 459 | ); 460 | } 461 | 462 | // 463 | // google 464 | // 465 | googleAuth(req, res) { 466 | // For eg. "http://localhost:3000/google/callback" 467 | const params = { 468 | redirect_uri: makeRedirectUri(req, this.googleRedirectUri), 469 | scope: "email profile", 470 | response_type: "code" 471 | }; 472 | console.log("params", params); 473 | return res.redirect(this.googleOAuth2.getAuthorizeUrl(params)); 474 | } 475 | 476 | googleCallback(req, res) { 477 | if (req.error_reason) { 478 | res.send(req.error_reason); 479 | } 480 | if (req.query && req.query.code) { 481 | // For eg. "/google/callback" 482 | this.googleOAuth2.getOAuthAccessToken( 483 | req.query.code, 484 | { 485 | grant_type: "authorization_code", 486 | redirect_uri: makeRedirectUri(req, this.googleRedirectUri) 487 | }, 488 | (err, accessToken, refreshToken, params) => { 489 | if (err) { 490 | console.error(err); 491 | return res.send(err); 492 | } 493 | 494 | const googleAuth = { 495 | access_token: accessToken, 496 | expiration_date: params.expires_in 497 | }; 498 | // when custom callback 499 | return callbackResult(req, res, googleAuth); 500 | } 501 | ); 502 | } 503 | } 504 | 505 | /** 506 | * @param {String} accessToken 507 | * @return {Object} parse user 508 | */ 509 | googleLogin(req, res) { 510 | const { body = {}, session = {} } = req; 511 | console.log("body", body); 512 | console.log("session", session); 513 | const accessToken = body.access_token || session.access_token; 514 | const expires = body.expiration_date || session.expiration_date; 515 | if (!accessToken) 516 | return res 517 | .status(500) 518 | .json({ code: 101, error: "Invalid google access_token" }) 519 | .end(); 520 | 521 | function errorFn(err) { 522 | console.error(err); 523 | return res 524 | .status(500) 525 | .json(err) 526 | .end(); 527 | } 528 | 529 | // https://developers.google.com/oauthplayground 530 | this.googleOAuth2.get( 531 | "https://www.googleapis.com/oauth2/v2/userinfo", 532 | accessToken, 533 | (err, data /* , response */) => { 534 | if (err) { 535 | return errorFn(err); 536 | } 537 | 538 | const profile = JSON.parse(data); 539 | console.log(profile); 540 | const profileImageUrl = profile.picture; 541 | 542 | const authData = { 543 | google: { 544 | id: profile.id, 545 | access_token: accessToken, 546 | expiration_date: expires 547 | } 548 | }; 549 | 550 | if (!profile.email) 551 | return errorFn({ code: 101, error: "Email is unknown" }); 552 | 553 | const parseRest = new ParseRest(req); 554 | parseRest 555 | .get( 556 | "/users", 557 | { where: { username: profile.email } }, 558 | { useMasterKey: true } 559 | ) 560 | .then(users => { 561 | if (users && users[0]) { 562 | // Retrieving 563 | const user = users[0]; 564 | // ban user 565 | if (user.isBanned) 566 | return errorFn({ code: 101, error: "User is banned" }); 567 | // save param 568 | const _param = { socialType: "google", authData }; 569 | parseRest 570 | .put("/users/" + user.objectId, _param, { useMasterKey: true }) 571 | .then(() => { 572 | // session query 573 | parseRest 574 | .get( 575 | "/sessions", 576 | { 577 | where: { 578 | user: { 579 | __type: "Pointer", 580 | className: "_User", 581 | objectId: user.objectId 582 | } 583 | } 584 | }, 585 | { useMasterKey: true } 586 | ) 587 | .then(sessions => { 588 | if (sessions && sessions[0]) { 589 | const _session = sessions[0]; 590 | if (typeof req.session === "object") 591 | req.session.sessionToken = _session.sessionToken; 592 | // end 593 | return this.userHandler(req, { 594 | ...user, 595 | ..._param, 596 | sessionToken: _session.sessionToken 597 | }).then(handledUser => res.json(handledUser)); 598 | } 599 | // login 600 | const password = 601 | typeof profile.id === "number" 602 | ? profile.id.toString() 603 | : profile.id; 604 | return parseRest 605 | .put( 606 | "/users/" + user.objectId, 607 | { password }, 608 | { useMasterKey: true } 609 | ) 610 | .then(() => { 611 | return parseRest 612 | .get("/login", { 613 | username: profile.email, 614 | password 615 | }) 616 | .then(result => { 617 | // reload 618 | parseRest 619 | .get("/users/me", null, { 620 | "X-Parse-Session-Token": result.sessionToken 621 | }) 622 | .then(_user => { 623 | // end 624 | return this.userHandler(req, { 625 | ..._user, 626 | sessionToken: result.sessionToken 627 | }).then(handledUser => res.json(handledUser)); 628 | }, errorFn); 629 | }, errorFn); 630 | }, errorFn); 631 | }, errorFn); 632 | }, errorFn); 633 | } else { 634 | // New 635 | const user = { 636 | username: profile.email, 637 | password: 638 | typeof profile.id === "number" 639 | ? profile.id.toString() 640 | : profile.id, 641 | name: profile.name, 642 | email: profile.email, 643 | socialType: "google", 644 | socialProfile: profile, 645 | profileImage: { url: profileImageUrl }, 646 | authData 647 | }; 648 | parseRest 649 | .post("/users", user, { useMasterKey: true }) 650 | .then(result => { 651 | // reload 652 | parseRest 653 | .get("/users/me", null, { 654 | "X-Parse-Session-Token": result.sessionToken 655 | }) 656 | .then(_user => { 657 | // end 658 | return this.userHandler(req, { 659 | ..._user, 660 | sessionToken: result.sessionToken 661 | }).then(handledUser => res.json(handledUser)); 662 | }, errorFn); 663 | }, errorFn); 664 | } 665 | }, errorFn); 666 | } 667 | ); 668 | } 669 | 670 | // 671 | // instagram 672 | // 673 | instagramAuth(req, res) { 674 | // For eg. "http://localhost:3000/instagram/callback" 675 | const params = { 676 | redirect_uri: makeRedirectUri(req, this.instaRedirectUri), 677 | scope: "basic public_content", 678 | response_type: "code" 679 | }; 680 | console.log("params", params); 681 | return res.redirect(this.instaOAuth2.getAuthorizeUrl(params)); 682 | } 683 | 684 | instagramCallback(req, res) { 685 | if (req.error_reason) { 686 | res.send(req.error_reason); 687 | } 688 | if (req.query && req.query.code) { 689 | // For eg. "/instagram/callback" 690 | this.instaOAuth2.getOAuthAccessToken( 691 | req.query.code, 692 | { 693 | grant_type: "authorization_code", 694 | redirect_uri: makeRedirectUri(req, this.instaRedirectUri) 695 | }, 696 | (err, accessToken, refreshToken, params) => { 697 | if (err) { 698 | console.error(err); 699 | return res.send(err); 700 | } 701 | 702 | const instagramAuth = { 703 | access_token: accessToken, 704 | user: params.user 705 | }; 706 | // when custom callback 707 | return callbackResult(req, res, instagramAuth); 708 | } 709 | ); 710 | } 711 | } 712 | 713 | /** 714 | * @param {String} accessToken 715 | * @return {Object} parse user 716 | */ 717 | instagramLogin(req, res) { 718 | const { body = {}, session = {} } = req; 719 | console.log("body", body); 720 | console.log("session", session); 721 | const accessToken = body.access_token || session.access_token; 722 | if (!accessToken) 723 | return res 724 | .status(500) 725 | .json({ code: 101, error: "Invalid instagram access_token" }) 726 | .end(); 727 | 728 | function errorFn(err) { 729 | console.error(err); 730 | return res 731 | .status(500) 732 | .json(err) 733 | .end(); 734 | } 735 | 736 | // https://www.instagram.com/developer/endpoints/users/ 737 | this.instaOAuth2.get( 738 | "https://api.instagram.com/v1/users/self/", 739 | accessToken, 740 | (err, data /* , response */) => { 741 | if (err) { 742 | return errorFn(err); 743 | } 744 | 745 | const profile = JSON.parse(data).data; 746 | console.log(profile); 747 | 748 | const authData = { 749 | instagram: { 750 | id: profile.id, 751 | access_token: accessToken 752 | } 753 | }; 754 | 755 | if (!profile.username) 756 | return errorFn({ code: 101, error: "Email is unknown" }); 757 | 758 | const parseRest = new ParseRest(req); 759 | parseRest 760 | .get( 761 | "/users", 762 | { where: { username: profile.username } }, 763 | { useMasterKey: true } 764 | ) 765 | .then(users => { 766 | if (users && users[0]) { 767 | // Retrieving 768 | const user = users[0]; 769 | // ban user 770 | if (user.isBanned) 771 | return errorFn({ code: 101, error: "User is banned" }); 772 | // save param 773 | const _param = { socialType: "instagram", authData }; 774 | parseRest 775 | .put("/users/" + user.objectId, _param, { useMasterKey: true }) 776 | .then(() => { 777 | // session query 778 | parseRest 779 | .get( 780 | "/sessions", 781 | { 782 | where: { 783 | user: { 784 | __type: "Pointer", 785 | className: "_User", 786 | objectId: user.objectId 787 | } 788 | } 789 | }, 790 | { useMasterKey: true } 791 | ) 792 | .then(sessions => { 793 | if (sessions && sessions[0]) { 794 | const _session = sessions[0]; 795 | if (typeof req.session === "object") 796 | req.session.sessionToken = _session.sessionToken; 797 | // end 798 | return this.userHandler(req, { 799 | ...user, 800 | ..._param, 801 | sessionToken: _session.sessionToken 802 | }).then(handledUser => res.json(handledUser)); 803 | } 804 | // login 805 | const password = 806 | typeof profile.id === "number" 807 | ? profile.id.toString() 808 | : profile.id; 809 | return parseRest 810 | .put( 811 | "/users/" + user.objectId, 812 | { password }, 813 | { useMasterKey: true } 814 | ) 815 | .then(() => { 816 | return parseRest 817 | .get("/login", { 818 | username: profile.username, 819 | password 820 | }) 821 | .then(result => { 822 | // reload 823 | parseRest 824 | .get("/users/me", null, { 825 | "X-Parse-Session-Token": result.sessionToken 826 | }) 827 | .then(_user => { 828 | // end 829 | return this.userHandler(req, { 830 | ..._user, 831 | sessionToken: result.sessionToken 832 | }).then(handledUser => res.json(handledUser)); 833 | }, errorFn); 834 | }, errorFn); 835 | }, errorFn); 836 | }, errorFn); 837 | }, errorFn); 838 | } else { 839 | // New 840 | const user = { 841 | username: profile.username, 842 | password: 843 | typeof profile.id === "number" 844 | ? profile.id.toString() 845 | : profile.id, 846 | name: profile.full_name, 847 | // email: profile.email, 848 | socialType: "instagram", 849 | socialProfile: profile, 850 | profileImage: { url: profile.profile_picture }, 851 | authData 852 | }; 853 | parseRest 854 | .post("/users", user, { useMasterKey: true }) 855 | .then(result => { 856 | // reload 857 | parseRest 858 | .get("/users/me", null, { 859 | "X-Parse-Session-Token": result.sessionToken 860 | }) 861 | .then(_user => { 862 | // end 863 | return this.userHandler(req, { 864 | ..._user, 865 | sessionToken: result.sessionToken 866 | }).then(handledUser => res.json(handledUser)); 867 | }, errorFn); 868 | }, errorFn); 869 | } 870 | }, errorFn); 871 | } 872 | ); 873 | } 874 | 875 | /** 876 | * @param {String} accessToken 877 | * @return {Object} parse user 878 | */ 879 | instagramLink(req, res) { 880 | const { body = {}, session = {} } = req; 881 | console.log("body", body); 882 | console.log("session", session); 883 | const accessToken = body.access_token || session.access_token; 884 | const userId = body.userId || (session.user && session.user.objectId); 885 | const username = body.username || (session.user && session.user.username); 886 | if (!accessToken) 887 | return res 888 | .status(500) 889 | .json({ code: 101, error: "Invalid instagram access_token" }) 890 | .end(); 891 | if (!userId && !username) 892 | return res 893 | .status(500) 894 | .json({ code: 102, error: "Invalid parameter : userId or username" }) 895 | .end(); 896 | 897 | function errorFn(err) { 898 | console.error(err); 899 | return res 900 | .status(500) 901 | .json(err) 902 | .end(); 903 | } 904 | 905 | this.instaOAuth2.get( 906 | "https://api.instagram.com/v1/users/self/", 907 | accessToken, 908 | (err, data /* , response */) => { 909 | if (err) { 910 | console.error(err); 911 | res.send(err); 912 | } else { 913 | const profile = JSON.parse(data).data; 914 | console.log(profile); 915 | 916 | const authData = { 917 | instagram: { 918 | id: profile.id, 919 | access_token: accessToken 920 | } 921 | }; 922 | 923 | const parseRest = new ParseRest(req); 924 | const _where = userId ? { objectId: userId } : { username }; 925 | parseRest 926 | .get("/users", { where: _where }, { useMasterKey: true }) 927 | .then(users => { 928 | if (users && users[0]) { 929 | // Retrieving 930 | const user = users[0]; 931 | // authData save 932 | const newAuthData = { ...user.authData, ...authData }; 933 | return parseRest 934 | .put( 935 | "/users/" + user.objectId, 936 | { authData: newAuthData }, 937 | { useMasterKey: true } 938 | ) 939 | .then(() => { 940 | // keep 941 | user.authData = newAuthData; 942 | // session query 943 | parseRest 944 | .get( 945 | "/sessions", 946 | { 947 | where: { 948 | user: { 949 | __type: "Pointer", 950 | className: "_User", 951 | objectId: user.objectId 952 | } 953 | } 954 | }, 955 | { useMasterKey: true } 956 | ) 957 | .then(sessions => { 958 | if (sessions && sessions[0]) { 959 | const _session = sessions[0]; 960 | if (typeof req.session === "object") 961 | req.session.sessionToken = _session.sessionToken; 962 | // end 963 | return this.userHandler(req, { 964 | ...user, 965 | ..._param, 966 | sessionToken: _session.sessionToken 967 | }).then(handledUser => res.json(handledUser)); 968 | } 969 | // login 970 | const password = 971 | typeof profile.id === "number" 972 | ? profile.id.toString() 973 | : profile.id; 974 | return parseRest 975 | .put( 976 | "/users/" + user.objectId, 977 | { password }, 978 | { useMasterKey: true } 979 | ) 980 | .then(() => { 981 | return parseRest 982 | .get("/login", { 983 | username, 984 | password 985 | }) 986 | .then(result => { 987 | // reload 988 | parseRest 989 | .get("/users/me", null, { 990 | "X-Parse-Session-Token": result.sessionToken 991 | }) 992 | .then(_user => { 993 | // end 994 | return this.userHandler(req, { 995 | ..._user, 996 | sessionToken: result.sessionToken 997 | }).then(handledUser => 998 | res.json(handledUser) 999 | ); 1000 | }, errorFn); 1001 | }, errorFn); 1002 | }, errorFn); 1003 | }, errorFn); 1004 | }, errorFn); 1005 | } 1006 | return errorFn({ code: 101, error: "user not exist" }); 1007 | }, errorFn); 1008 | } 1009 | } 1010 | ); 1011 | } 1012 | 1013 | /** 1014 | * @param {String} userId 1015 | * @return {Array} instagram recent media 1016 | */ 1017 | instagramRecent(req, res) { 1018 | const { query = {}, session = {} } = req; 1019 | console.log("query", query); 1020 | console.log("session", session); 1021 | const userId = query.userId || (session.user && session.user.objectId); 1022 | if (!userId) 1023 | return res 1024 | .status(500) 1025 | .json({ code: 102, error: "Invalid parameter : userId" }) 1026 | .end(); 1027 | 1028 | function errorFn(err) { 1029 | console.error(err); 1030 | return res 1031 | .status(500) 1032 | .json(err) 1033 | .end(); 1034 | } 1035 | 1036 | const parseRest = new ParseRest(req); 1037 | parseRest 1038 | .get("/users", { where: { objectId: userId } }, { useMasterKey: true }) 1039 | .then(users => { 1040 | if (users && users[0]) { 1041 | // Retrieving 1042 | const user = users[0]; 1043 | // get instagram authData 1044 | const accessToken = 1045 | user.authData && 1046 | user.authData.instagram && 1047 | user.authData.instagram.access_token; 1048 | if (!accessToken) 1049 | return errorFn({ 1050 | code: 101, 1051 | error: "Invalid instagram access_token" 1052 | }); 1053 | 1054 | // get recent 1055 | return this.instaOAuth2.get( 1056 | "https://api.instagram.com/v1/users/self/media/recent/", 1057 | accessToken, 1058 | (err, data /* , response */) => { 1059 | if (err) { 1060 | return errorFn(err); 1061 | } 1062 | 1063 | const recent = JSON.parse(data).data; 1064 | // end 1065 | return res.json(recent); 1066 | } 1067 | ); 1068 | } 1069 | return errorFn("user not exist"); 1070 | }); 1071 | } 1072 | 1073 | // 1074 | // naver 1075 | // 1076 | naverAuth(req, res) { 1077 | // For eg. "http://localhost:3000/naver/callback" 1078 | const params = { 1079 | redirect_uri: makeRedirectUri(req, this.naverRedirectUri), 1080 | response_type: "code" 1081 | }; 1082 | console.log("params", params); 1083 | return res.redirect(this.naverOAuth2.getAuthorizeUrl(params)); 1084 | } 1085 | 1086 | naverCallback(req, res) { 1087 | if (req.error_reason) { 1088 | res.send(req.error_reason); 1089 | } 1090 | if (req.query && req.query.code) { 1091 | // For eg. "/naver/callback" 1092 | this.naverOAuth2.getOAuthAccessToken( 1093 | req.query.code, 1094 | { 1095 | grant_type: "authorization_code", 1096 | redirect_uri: makeRedirectUri(req, this.naverRedirectUri) 1097 | }, 1098 | (err, accessToken, refreshToken, params) => { 1099 | if (err) { 1100 | console.error(err); 1101 | return res.send(err); 1102 | } 1103 | 1104 | const naverAuth = { 1105 | access_token: accessToken, 1106 | expiration_date: params.expires_in 1107 | }; 1108 | // when custom callback 1109 | return callbackResult(req, res, naverAuth); 1110 | } 1111 | ); 1112 | } 1113 | } 1114 | 1115 | /** 1116 | * @param {String} accessToken 1117 | * @return {Object} parse user 1118 | */ 1119 | naverLogin(req, res) { 1120 | const { body = {}, session = {} } = req; 1121 | console.log("body", body); 1122 | console.log("session", session); 1123 | const accessToken = body.access_token || session.access_token; 1124 | const expires = body.expiration_date || session.expiration_date; 1125 | if (!accessToken) 1126 | return res 1127 | .status(500) 1128 | .json({ code: 101, error: "Invalid naver access_token" }) 1129 | .end(); 1130 | 1131 | function errorFn(err) { 1132 | console.error(err); 1133 | return res 1134 | .status(500) 1135 | .json(err) 1136 | .end(); 1137 | } 1138 | 1139 | // https://developers.naver.com/docs/login/profile/ 1140 | this.naverOAuth2.get("https://openapi.naver.com/v1/nid/me", accessToken, ( 1141 | err, 1142 | data /* , response */ 1143 | ) => { 1144 | if (err) { 1145 | return errorFn(err); 1146 | } 1147 | 1148 | const profile = JSON.parse(data).response; 1149 | console.log(profile); 1150 | 1151 | const authDataEtc = { 1152 | naver: { 1153 | id: profile.id, 1154 | access_token: accessToken, 1155 | expiration_date: expires 1156 | } 1157 | }; 1158 | 1159 | if (!profile.email) 1160 | return errorFn({ code: 101, error: "Email is unknown" }); 1161 | 1162 | const parseRest = new ParseRest(req); 1163 | parseRest 1164 | .get( 1165 | "/users", 1166 | { where: { username: profile.email } }, 1167 | { useMasterKey: true } 1168 | ) 1169 | .then(users => { 1170 | if (users && users[0]) { 1171 | // Retrieving 1172 | const user = users[0]; 1173 | // ban user 1174 | if (user.isBanned) 1175 | return errorFn({ code: 101, error: "User is banned" }); 1176 | // save param 1177 | const _param = { socialType: "naver", authDataEtc }; 1178 | parseRest 1179 | .put("/users/" + user.objectId, _param, { useMasterKey: true }) 1180 | .then(() => { 1181 | // session query 1182 | parseRest 1183 | .get( 1184 | "/sessions", 1185 | { 1186 | where: { 1187 | user: { 1188 | __type: "Pointer", 1189 | className: "_User", 1190 | objectId: user.objectId 1191 | } 1192 | } 1193 | }, 1194 | { useMasterKey: true } 1195 | ) 1196 | .then(sessions => { 1197 | if (sessions && sessions[0]) { 1198 | const _session = sessions[0]; 1199 | if (typeof req.session === "object") 1200 | req.session.sessionToken = _session.sessionToken; 1201 | // end 1202 | return this.userHandler(req, { 1203 | ...user, 1204 | ..._param, 1205 | sessionToken: _session.sessionToken 1206 | }).then(handledUser => res.json(handledUser)); 1207 | } 1208 | // login 1209 | const password = 1210 | typeof profile.id === "number" 1211 | ? profile.id.toString() 1212 | : profile.id; 1213 | return parseRest 1214 | .put( 1215 | "/users/" + user.objectId, 1216 | { password }, 1217 | { useMasterKey: true } 1218 | ) 1219 | .then(() => { 1220 | return parseRest 1221 | .get("/login", { 1222 | username: profile.email, 1223 | password 1224 | }) 1225 | .then(result => { 1226 | // reload 1227 | parseRest 1228 | .get("/users/me", null, { 1229 | "X-Parse-Session-Token": result.sessionToken 1230 | }) 1231 | .then(_user => { 1232 | // end 1233 | return this.userHandler(req, { 1234 | ..._user, 1235 | sessionToken: result.sessionToken 1236 | }).then(handledUser => res.json(handledUser)); 1237 | }, errorFn); 1238 | }, errorFn); 1239 | }, errorFn); 1240 | }, errorFn); 1241 | }, errorFn); 1242 | } else { 1243 | // New 1244 | const user = { 1245 | username: profile.email, 1246 | password: 1247 | typeof profile.id === "number" 1248 | ? profile.id.toString() 1249 | : profile.id, 1250 | name: profile.name, 1251 | email: profile.email, 1252 | socialType: "naver", 1253 | socialProfile: profile, 1254 | profileImage: { url: profile.profile_image }, 1255 | authDataEtc 1256 | }; 1257 | parseRest 1258 | .post("/users", user, { useMasterKey: true }) 1259 | .then(result => { 1260 | // reload 1261 | parseRest 1262 | .get("/users/me", null, { 1263 | "X-Parse-Session-Token": result.sessionToken 1264 | }) 1265 | .then(_user => { 1266 | // end 1267 | return this.userHandler(req, { 1268 | ..._user, 1269 | sessionToken: result.sessionToken 1270 | }).then(handledUser => res.json(handledUser)); 1271 | }, errorFn); 1272 | }, errorFn); 1273 | } 1274 | }, errorFn); 1275 | }); 1276 | } 1277 | 1278 | // 1279 | // daum 1280 | // 1281 | daumAuth(req, res) { 1282 | // For eg. "http://localhost:3000/daum/callback" 1283 | const params = { 1284 | redirect_uri: makeRedirectUri(req, this.daumRedirectUri), 1285 | response_type: "code" 1286 | }; 1287 | console.log("params", params); 1288 | return res.redirect(this.daumOAuth2.getAuthorizeUrl(params)); 1289 | } 1290 | 1291 | daumCallback(req, res) { 1292 | if (req.error_reason) { 1293 | res.send(req.error_reason); 1294 | } 1295 | if (req.query && req.query.code) { 1296 | // For eg. "/daum/callback" 1297 | this.daumOAuth2.getOAuthAccessToken( 1298 | req.query.code, 1299 | { 1300 | grant_type: "authorization_code", 1301 | redirect_uri: makeRedirectUri(req, this.daumRedirectUri) 1302 | }, 1303 | (err, accessToken, refreshToken, params) => { 1304 | if (err) { 1305 | console.error(err); 1306 | return res.send(err); 1307 | } 1308 | 1309 | const daumAuth = { 1310 | access_token: accessToken, 1311 | expiration_date: params.expires_in 1312 | }; 1313 | // when custom callback 1314 | return callbackResult(req, res, daumAuth); 1315 | } 1316 | ); 1317 | } 1318 | } 1319 | 1320 | /** 1321 | * @param {String} accessToken 1322 | * @return {Object} parse user 1323 | */ 1324 | daumLogin(req, res) { 1325 | const { body = {}, session = {} } = req; 1326 | console.log("body", body); 1327 | console.log("session", session); 1328 | const accessToken = body.access_token || session.access_token; 1329 | const expires = body.expiration_date || session.expiration_date; 1330 | if (!accessToken) 1331 | return res 1332 | .status(500) 1333 | .json({ code: 101, error: "Invalid daum access_token" }) 1334 | .end(); 1335 | 1336 | function errorFn(err) { 1337 | console.error(err); 1338 | return res 1339 | .status(500) 1340 | .json(err) 1341 | .end(); 1342 | } 1343 | 1344 | // https://developers.daum.net/services/apis/user/v1/show.format 1345 | this.daumOAuth2.get( 1346 | "https://apis.daum.net/user/v1/show.json", 1347 | accessToken, 1348 | (err, data /* , response */) => { 1349 | if (err) { 1350 | return errorFn(err); 1351 | } 1352 | 1353 | const profile = JSON.parse(data).result; 1354 | console.log(profile); 1355 | 1356 | const authDataEtc = { 1357 | daum: { 1358 | id: profile.id, 1359 | access_token: accessToken, 1360 | expiration_date: expires 1361 | } 1362 | }; 1363 | 1364 | if (!profile.userid) 1365 | return errorFn({ code: 101, error: "Email is unknown" }); 1366 | 1367 | if (req.headers) req.headers.sessionToken = null; 1368 | if (req.session) req.session.sessionToken = null; 1369 | const parseRest = new ParseRest(req); 1370 | parseRest 1371 | .get( 1372 | "/users", 1373 | { where: { username: profile.userid } }, 1374 | { useMasterKey: true } 1375 | ) 1376 | .then(users => { 1377 | if (users && users[0]) { 1378 | // Retrieving 1379 | const user = users[0]; 1380 | // ban user 1381 | if (user.isBanned) 1382 | return errorFn({ code: 101, error: "User is banned" }); 1383 | // save param 1384 | const _param = { socialType: "daum", authDataEtc }; 1385 | parseRest 1386 | .put("/users/" + user.objectId, _param, { useMasterKey: true }) 1387 | .then(() => { 1388 | // session query 1389 | parseRest 1390 | .get( 1391 | "/sessions", 1392 | { 1393 | where: { 1394 | user: { 1395 | __type: "Pointer", 1396 | className: "_User", 1397 | objectId: user.objectId 1398 | } 1399 | } 1400 | }, 1401 | { useMasterKey: true } 1402 | ) 1403 | .then(sessions => { 1404 | if (sessions && sessions[0]) { 1405 | const _session = sessions[0]; 1406 | if (typeof req.session === "object") 1407 | req.session.sessionToken = _session.sessionToken; 1408 | // end 1409 | return this.userHandler(req, { 1410 | ...user, 1411 | ..._param, 1412 | sessionToken: _session.sessionToken 1413 | }).then(handledUser => res.json(handledUser)); 1414 | } 1415 | // login 1416 | const password = 1417 | typeof profile.id === "number" 1418 | ? profile.id.toString() 1419 | : profile.id; 1420 | return parseRest 1421 | .put( 1422 | "/users/" + user.objectId, 1423 | { password }, 1424 | { useMasterKey: true } 1425 | ) 1426 | .then(() => { 1427 | return parseRest 1428 | .get("/login", { 1429 | username: profile.userid, 1430 | password 1431 | }) 1432 | .then(result => { 1433 | // reload 1434 | parseRest 1435 | .get("/users/me", null, { 1436 | "X-Parse-Session-Token": result.sessionToken 1437 | }) 1438 | .then(_user => { 1439 | // end 1440 | return this.userHandler(req, { 1441 | ..._user, 1442 | sessionToken: result.sessionToken 1443 | }).then(handledUser => res.json(handledUser)); 1444 | }, errorFn); 1445 | }, errorFn); 1446 | }, errorFn); 1447 | }, errorFn); 1448 | }, errorFn); 1449 | } else { 1450 | // New 1451 | const user = { 1452 | username: profile.userid, 1453 | password: 1454 | typeof profile.id === "number" 1455 | ? profile.id.toString() 1456 | : profile.id, 1457 | name: profile.nickname, 1458 | // email: profile.email, 1459 | socialType: "daum", 1460 | socialProfile: profile, 1461 | profileImage: { url: profile.imagePath }, 1462 | authDataEtc 1463 | }; 1464 | parseRest 1465 | .post("/users", user, { useMasterKey: true }) 1466 | .then(result => { 1467 | // reload 1468 | parseRest 1469 | .get("/users/me", null, { 1470 | "X-Parse-Session-Token": result.sessionToken 1471 | }) 1472 | .then(_user => { 1473 | // end 1474 | return this.userHandler(req, { 1475 | ..._user, 1476 | sessionToken: result.sessionToken 1477 | }).then(handledUser => res.json(handledUser)); 1478 | }, errorFn); 1479 | }, errorFn); 1480 | } 1481 | }, errorFn); 1482 | } 1483 | ); 1484 | } 1485 | 1486 | // 1487 | // kakao 1488 | // 1489 | kakaoAuth(req, res) { 1490 | // For eg. "http://localhost:3000/kakao/callback" 1491 | const params = { 1492 | redirect_uri: makeRedirectUri(req, this.kakaoRedirectUri), 1493 | response_type: "code" 1494 | }; 1495 | console.log("params", params); 1496 | return res.redirect(this.kakaoOAuth2.getAuthorizeUrl(params)); 1497 | } 1498 | 1499 | kakaoCallback(req, res) { 1500 | if (req.error_reason) { 1501 | res.send(req.error_reason); 1502 | } 1503 | if (req.query && req.query.code) { 1504 | // For eg. "/kakao/callback" 1505 | this.kakaoOAuth2.getOAuthAccessToken( 1506 | req.query.code, 1507 | { 1508 | grant_type: "authorization_code", 1509 | redirect_uri: makeRedirectUri(req, this.kakaoRedirectUri) 1510 | }, 1511 | (err, accessToken, refreshToken, params) => { 1512 | if (err) { 1513 | console.error(err); 1514 | return res.send(err); 1515 | } 1516 | 1517 | const kakaoAuth = { 1518 | access_token: accessToken, 1519 | expiration_date: params.expires_in 1520 | }; 1521 | // when custom callback 1522 | return callbackResult(req, res, kakaoAuth); 1523 | } 1524 | ); 1525 | } 1526 | } 1527 | 1528 | /** 1529 | * @param {String} accessToken 1530 | * @return {Object} parse user 1531 | */ 1532 | kakaoLogin(req, res) { 1533 | const { body = {}, session = {} } = req; 1534 | console.log("body", body); 1535 | console.log("session", session); 1536 | const accessToken = body.access_token || session.access_token; 1537 | const expires = body.expiration_date || session.expiration_date; 1538 | if (!accessToken) 1539 | return res 1540 | .status(500) 1541 | .json({ code: 101, error: "Invalid kakao access_token" }) 1542 | .end(); 1543 | 1544 | function errorFn(err) { 1545 | console.error(err); 1546 | return res 1547 | .status(500) 1548 | .json(err) 1549 | .end(); 1550 | } 1551 | 1552 | // https://developers.kakao.com/docs/restapi/user-management#%EB%A1%9C%EA%B7%B8%EC%9D%B8 1553 | this.kakaoOAuth2.get("https://kapi.kakao.com/v1/user/me", accessToken, ( 1554 | err, 1555 | data /* , response */ 1556 | ) => { 1557 | if (err) { 1558 | return errorFn(err); 1559 | } 1560 | 1561 | const profile = JSON.parse(data); 1562 | console.log(profile); 1563 | 1564 | const authDataEtc = { 1565 | kakao: { 1566 | id: profile.id, 1567 | access_token: accessToken, 1568 | expiration_date: expires 1569 | } 1570 | }; 1571 | 1572 | if (!profile.kaccount_email && !profile.id) 1573 | return errorFn({ code: 101, error: "Email is unknown" }); 1574 | 1575 | if (req.headers) req.headers.sessionToken = null; 1576 | if (req.session) req.session.sessionToken = null; 1577 | const parseRest = new ParseRest(req); 1578 | parseRest 1579 | .get( 1580 | "/users", 1581 | { where: { username: profile.kaccount_email || profile.id } }, 1582 | { useMasterKey: true } 1583 | ) 1584 | .then(users => { 1585 | if (users && users[0]) { 1586 | // Retrieving 1587 | const user = users[0]; 1588 | // ban user 1589 | if (user.isBanned) 1590 | return errorFn({ code: 101, error: "User is banned" }); 1591 | // save param 1592 | const _param = { socialType: "kakao", authDataEtc }; 1593 | parseRest 1594 | .put("/users/" + user.objectId, _param, { useMasterKey: true }) 1595 | .then(() => { 1596 | // session query 1597 | parseRest 1598 | .get( 1599 | "/sessions", 1600 | { 1601 | where: { 1602 | user: { 1603 | __type: "Pointer", 1604 | className: "_User", 1605 | objectId: user.objectId 1606 | } 1607 | } 1608 | }, 1609 | { useMasterKey: true } 1610 | ) 1611 | .then(sessions => { 1612 | if (sessions && sessions[0]) { 1613 | const _session = sessions[0]; 1614 | if (typeof req.session === "object") 1615 | req.session.sessionToken = _session.sessionToken; 1616 | // end 1617 | return this.userHandler(req, { 1618 | ...user, 1619 | ..._param, 1620 | sessionToken: _session.sessionToken 1621 | }).then(handledUser => res.json(handledUser)); 1622 | } 1623 | // login 1624 | const password = 1625 | typeof profile.id === "number" 1626 | ? profile.id.toString() 1627 | : profile.id; 1628 | return parseRest 1629 | .put( 1630 | "/users/" + user.objectId, 1631 | { password }, 1632 | { useMasterKey: true } 1633 | ) 1634 | .then(() => { 1635 | return parseRest 1636 | .get("/login", { 1637 | username: profile.kaccount_email || profile.id, 1638 | password 1639 | }) 1640 | .then(result => { 1641 | // reload 1642 | parseRest 1643 | .get("/users/me", null, { 1644 | "X-Parse-Session-Token": result.sessionToken 1645 | }) 1646 | .then(_user => { 1647 | // end 1648 | return this.userHandler(req, { 1649 | ..._user, 1650 | sessionToken: result.sessionToken 1651 | }).then(handledUser => res.json(handledUser)); 1652 | }, errorFn); 1653 | }, errorFn); 1654 | }, errorFn); 1655 | }, errorFn); 1656 | }, errorFn); 1657 | } else { 1658 | // New 1659 | const user = { 1660 | username: profile.kaccount_email || profile.id, 1661 | password: 1662 | typeof profile.id === "number" 1663 | ? profile.id.toString() 1664 | : profile.id, 1665 | name: profile.properties.nickname, 1666 | // email: profile.email, 1667 | socialType: "kakao", 1668 | socialProfile: profile, 1669 | profileImage: { url: profile.properties.profile_image }, 1670 | authDataEtc 1671 | }; 1672 | parseRest 1673 | .post("/users", user, { useMasterKey: true }) 1674 | .then(result => { 1675 | // reload 1676 | parseRest 1677 | .get("/users/me", null, { 1678 | "X-Parse-Session-Token": result.sessionToken 1679 | }) 1680 | .then(_user => { 1681 | // end 1682 | return this.userHandler(req, { 1683 | ..._user, 1684 | sessionToken: result.sessionToken 1685 | }).then(handledUser => res.json(handledUser)); 1686 | }, errorFn); 1687 | }, errorFn); 1688 | } 1689 | }, errorFn); 1690 | }); 1691 | } 1692 | } 1693 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var session = require('express-session'); 3 | var bodyParser = require('body-parser'); 4 | var http = require('http'); 5 | var ParseServer = require('parse-server').ParseServer; 6 | 7 | var SocialOAuth2 = require('./lib/index').default; 8 | 9 | // configuration 10 | var port = 3030; 11 | process.env.SERVER_URL = 'http://localhost:' + port + '/parse'; 12 | process.env.APP_ID = "Y58uDAh9445hEaGtwaJ3GnAMI10Bpk3b"; 13 | process.env.MASTER_KEY = "jt9gh7frZ32Y5Q698RP87F55R41pPdx1"; 14 | process.env.FB_APPIDS = ["1360181184056097"]; 15 | process.env.FB_SECRETS = ["cc50007848c429374e89bd2c5202e404"]; 16 | process.env.GOOGLE_APPIDS = ["163894218564-8gskurdh9gkm1ba1a5rm5n922rdung80.apps.googleusercontent.com"]; 17 | process.env.GOOGLE_SECRETS = ["e4EYIMUxxVH5Gr7DY55libeR"]; 18 | process.env.INSTA_APPIDS = ["6b5bf7aef2eb4296961fe43af1858a3c"]; 19 | process.env.INSTA_SECRETS = ["dbbc1c52f4da47c6a86f2f081e82598c"]; 20 | process.env.NAVER_APPIDS = ["Uyjq1a8Vz0nngCdlMZZw"]; 21 | process.env.NAVER_SECRETS = ["_Xigkgo0SD"]; 22 | process.env.DAUM_APPIDS = ["6600734411403537733"]; 23 | process.env.DAUM_SECRETS = ["d5bfe986d88f43932736deb6a4aa1e09"]; 24 | process.env.KAKAO_RESTKEY = ["ba78f36569c3c34fc8af6aa324ddb499"]; 25 | process.env.KAKAO_SECRETS = ["1XyhYj0GXcY1nT0zlyTFQ3E16RLmpaet"]; 26 | 27 | // app 28 | var app = express(); 29 | 30 | // parse-server 31 | var api = new ParseServer({ 32 | databaseURI: 'mongodb://localhost/test', 33 | appId: process.env.APP_ID, 34 | masterKey: process.env.MASTER_KEY, 35 | serverURL: process.env.SERVER_URL, 36 | auth: { 37 | facebook: { 38 | appIds: process.env.FB_APPIDS 39 | } 40 | } 41 | }); 42 | 43 | // Serve the MiddleWare(Parse) on the /parse URL prefix 44 | app.use('/parse', api); 45 | 46 | var server = new http.Server(app); 47 | 48 | app.use(session({ 49 | secret: 'parse-oauth2-sns', 50 | resave: false, 51 | saveUninitialized: false, 52 | // cookie: { maxAge: 60000 } 53 | })); 54 | app.use(bodyParser.json()); 55 | 56 | // OAuth2 57 | app.use('/oauth2', SocialOAuth2.create({ path: '/oauth2' })); 58 | 59 | // default 60 | app.use((req, res) => { 61 | res.status(404).json({ code: 101, error: 'api not found' }).end(); 62 | }); 63 | 64 | // run server 65 | var runnable = app.listen(port, (err) => { 66 | if (err) { 67 | console.error(err); 68 | } 69 | console.info('----\n==> 🌎 API is running on port %s', port); 70 | console.info('==> 💻 Send requests to http://%s:%s', 'localhost', port); 71 | }); 72 | 73 | process.on('SIGINT', function() { 74 | console.log('SIGINT'); 75 | process.exit(); 76 | }); 77 | --------------------------------------------------------------------------------