From 71c3afbe36a21c5669aa89abf8cfebcb53b69e95 Mon Sep 17 00:00:00 2001 From: natjms Date: Thu, 29 Apr 2021 17:51:26 -0300 Subject: [PATCH] Implement user authentication --- package-lock.json | 187 +++++++++++++++++++++++++-- package.json | 3 +- src/components/pages/authenticate.js | 182 ++++++++++++++++++-------- 3 files changed, 309 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index 470f139..a26919a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,9 @@ "@react-navigation/native": "5.1.1", "@react-navigation/stack": "5.2.3", "expo": "^38.0.9", - "expo-linking": "^1.0.3", + "expo-linking": "^1.0.7", "expo-status-bar": "^1.0.2", + "expo-web-browser": "~8.3.1", "react": "~16.11.0", "react-dom": "~16.11.0", "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz", @@ -2867,6 +2868,17 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "node_modules/compare-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-urls/-/compare-urls-2.0.0.tgz", + "integrity": "sha512-eCJcWn2OYFEIqbm70ta7LQowJOOZZqq1a2YbbFCFI1uwSvj+TWMwXVn7vPR1ceFNcAIt5RSTDbwdlX82gYLTkA==", + "dependencies": { + "normalize-url": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/compare-versions": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", @@ -3610,13 +3622,25 @@ } }, "node_modules/expo-linking": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-1.0.3.tgz", - "integrity": "sha512-Bzm8qVlSRBmoQcnveBBLO4ea/AEW4t10WFiJ18m/w8OnEXbsP7WT39UyhTuFsrnjYSLMjW4/JXkfmswBOhISuA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-1.0.7.tgz", + "integrity": "sha512-AhvntaU1PItf73Tm5J7tUVEgCjz/7SeuXg4vLCQgFsEywU0E4Z/jgfQ+30omrrvalBKL3L18cHG9Y4pWmROqgg==", "dependencies": { - "expo-constants": "~9.1.1", + "expo-constants": "~9.2.0", "qs": "^6.5.0", "url-parse": "^1.4.4" + }, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/expo-linking/node_modules/expo-constants": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-9.2.0.tgz", + "integrity": "sha512-WKwiEMvBgPrEPEyZKm21UUB2KWQux9OCWf6ZDORLTln7kO3rsbaJEprfWUWTP7AxyaLMYfN+/0WFHjZc25SZWQ==", + "dependencies": { + "fbjs": "1.0.0", + "uuid": "^3.3.2" } }, "node_modules/expo-location": { @@ -3654,6 +3678,18 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.0.2.tgz", "integrity": "sha512-5313u744GcLzCadxIPXyTkYw77++UXv1dXCuhYDxDbtsEf93iMra7WSvzyE8a7mRQLIIPRuGnBOdrL/V1C7EOQ==" }, + "node_modules/expo-web-browser": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-8.3.1.tgz", + "integrity": "sha512-mDxSNpc/Ww/RX6MhmPRUWo2xNi8HGZ1TDMqIjTvUzrL7pGG9VerX0EDMhfLgo6c7KVOY1ngbTyybApZTXgPCOQ==", + "dependencies": { + "compare-urls": "^2.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/expo/node_modules/babel-preset-expo": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-8.2.3.tgz", @@ -4658,6 +4694,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -6203,6 +6247,40 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-url/node_modules/query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url/node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -6716,6 +6794,14 @@ "resolved": "https://registry.npmjs.org/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz", "integrity": "sha1-/mOhfal3YRq+98uAJssalVP9g1k=" }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "engines": { + "node": ">=4" + } + }, "node_modules/pretty-format": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", @@ -7980,6 +8066,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -11522,6 +11619,14 @@ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, + "compare-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-urls/-/compare-urls-2.0.0.tgz", + "integrity": "sha512-eCJcWn2OYFEIqbm70ta7LQowJOOZZqq1a2YbbFCFI1uwSvj+TWMwXVn7vPR1ceFNcAIt5RSTDbwdlX82gYLTkA==", + "requires": { + "normalize-url": "^2.0.1" + } + }, "compare-versions": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", @@ -12249,13 +12354,24 @@ } }, "expo-linking": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-1.0.3.tgz", - "integrity": "sha512-Bzm8qVlSRBmoQcnveBBLO4ea/AEW4t10WFiJ18m/w8OnEXbsP7WT39UyhTuFsrnjYSLMjW4/JXkfmswBOhISuA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-1.0.7.tgz", + "integrity": "sha512-AhvntaU1PItf73Tm5J7tUVEgCjz/7SeuXg4vLCQgFsEywU0E4Z/jgfQ+30omrrvalBKL3L18cHG9Y4pWmROqgg==", "requires": { - "expo-constants": "~9.1.1", + "expo-constants": "~9.2.0", "qs": "^6.5.0", "url-parse": "^1.4.4" + }, + "dependencies": { + "expo-constants": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-9.2.0.tgz", + "integrity": "sha512-WKwiEMvBgPrEPEyZKm21UUB2KWQux9OCWf6ZDORLTln7kO3rsbaJEprfWUWTP7AxyaLMYfN+/0WFHjZc25SZWQ==", + "requires": { + "fbjs": "1.0.0", + "uuid": "^3.3.2" + } + } } }, "expo-location": { @@ -12293,6 +12409,14 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.0.2.tgz", "integrity": "sha512-5313u744GcLzCadxIPXyTkYw77++UXv1dXCuhYDxDbtsEf93iMra7WSvzyE8a7mRQLIIPRuGnBOdrL/V1C7EOQ==" }, + "expo-web-browser": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-8.3.1.tgz", + "integrity": "sha512-mDxSNpc/Ww/RX6MhmPRUWo2xNi8HGZ1TDMqIjTvUzrL7pGG9VerX0EDMhfLgo6c7KVOY1ngbTyybApZTXgPCOQ==", + "requires": { + "compare-urls": "^2.0.0" + } + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -13034,6 +13158,11 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -14322,6 +14451,33 @@ "remove-trailing-separator": "^1.0.1" } }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "dependencies": { + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -14708,6 +14864,11 @@ "resolved": "https://registry.npmjs.org/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz", "integrity": "sha1-/mOhfal3YRq+98uAJssalVP9g1k=" }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, "pretty-format": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", @@ -15789,6 +15950,14 @@ } } }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", diff --git a/package.json b/package.json index fa0ed34..ede1137 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ "@react-navigation/native": "5.1.1", "@react-navigation/stack": "5.2.3", "expo": "^38.0.9", - "expo-linking": "^1.0.3", + "expo-linking": "^1.0.7", "expo-status-bar": "^1.0.2", + "expo-web-browser": "~8.3.1", "react": "~16.11.0", "react-dom": "~16.11.0", "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz", diff --git a/src/components/pages/authenticate.js b/src/components/pages/authenticate.js index aa48eee..5a665cf 100644 --- a/src/components/pages/authenticate.js +++ b/src/components/pages/authenticate.js @@ -10,41 +10,101 @@ import { } from "react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import * as Linking from "expo-linking"; +import * as WebBrowser from "expo-web-browser"; +import Constants from "expo-constants"; -const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg"; -const TEST_PROFILE = { - username: "njms", - acct: "njms", - display_name: "Nat🔆", - locked: false, - bot: false, - note: "Yeah heart emoji.", - avatar: TEST_IMAGE, - followers_count: "1 jillion", - statuses_count: 334, - fields: [ - { - name: "Blog", - value: "https://njms.ca", - verified_at: "some time" - }, - { - name: "Github", - value: "https://github.com/natjms", - verified_at: null - } - ] -}; +async function postForm(url, data, token = false) { + // Send a POST request with data formatted with FormData returning JSON + let form = new FormData(); + for (let key in data) { + form.append(key, data[key]); + } + + const response = await fetch(url, { + method: "POST", + body: form, + headers: token + ? { "Authorization": `Bearer ${token}`, } + : {}, + }); + + return response.json(); +} + +async function get(url, token = false) { + const response = await fetch(url, { + method: "GET", + headers: token + ? { "Authorization": `Bearer ${token}`, } + : {}, + }); + return response.json(); +} const AuthenticateJsx = ({navigation}) => { + const REDIRECT_URI = Linking.makeUrl("authenticate"); const [state, setState] = useState({ - acct: "", - password: "", + instance: "", authChecked: false, }); + const _handleUrl = async ({ url }) => { + // When the app is foregrounded after authorizing the app from their + // instance's website... + if (Constants.platform.ios) { + WebBrowser.dismissBrowser(); + } else { + Linking.removeEventListener("url", _handleUrl) + } + + const { path, queryParams } = Linking.parse(url); + + const instance = await AsyncStorage.getItem("@user_instance"); + const api = `https://${instance}`; + const app = JSON.parse( + await AsyncStorage.getItem("@app_object") + ); + + // Fetch the access token + const tokenRequestBody = { + client_id: app.client_id, + client_secret: app.client_secret, + redirect_uri: REDIRECT_URI, + grant_type: "authorization_code", + code: queryParams.code, + scope: "read write follow push", + }; + + const token = await postForm(`${api}/oauth/token`, tokenRequestBody); + + // Store the token + AsyncStorage.setItem("@user_token", JSON.stringify(token)); + + const profile = await get( + `${api}/api/v1/accounts/verify_credentials`, + token.access_token + ); + + await AsyncStorage.multiSet([ + [ "@user_profile", JSON.stringify(profile), ], + [ // TODO: Enable storing notifications + "@user_notifications", + JSON.stringify({ + unread: false, + memory: [] + }), + ], + ]); + + navigation.navigate("Feed"); + }; + useEffect(() => { - AsyncStorage.getItem("@user_profile").then((profile) => { + Linking.addEventListener("url", _handleUrl); + AsyncStorage + .getItem("@user_profile") + .then(profile => { if (profile) { navigation.navigate("Feed"); } else { @@ -53,21 +113,44 @@ const AuthenticateJsx = ({navigation}) => { }); }, []); - const loginCallback = () => { - const initialization = [ - [ "@user_profile", JSON.stringify(TEST_PROFILE) ], - [ - "@user_notifications", - JSON.stringify({ - unread: false, - memory: [{ id: 1 }, { id: 2 }], - }) - ] - ]; + const _login = async () => { + const url = `https://${state.instance}`; - AsyncStorage.multiSet(initialization).then(() => { - navigation.navigate("Feed"); - }); + let appJSON = await AsyncStorage.getItem("@app_object"); + let app; + + // Ensure the app has been created + if (appJSON == null) { + // Register app: https://docs.joinmastodon.org/methods/apps/#create-an-application + app = await postForm(`${url}/api/v1/apps`, { + client_name: "Resin", + redirect_uris: REDIRECT_URI, + scopes: "read write follow push", + website: "https://github.com/natjms/resin", + }); + + await AsyncStorage + .setItem("@app_object", JSON.stringify(app)) + } else { + // The app has already been registered + app = JSON.parse(appJSON); + } + + // Store the domain name of the instance for use in + // the _handleUrl callback + // NOTE: state.instance is not accessible from _handleUrl; this + // probably has something to do with the fact that the app loses + // focus when WebBrowser.openAuthSessionAsync gets called. + await AsyncStorage.setItem("@user_instance", state.instance); + + // Get the user to authorize the app + await WebBrowser.openAuthSessionAsync( + `${url}/oauth/authorize` + + `?client_id=${app.client_id}` + + `&scope=read+write+follow+push` + + `&redirect_uri=${REDIRECT_URI}` + + `&response_type=code` + ); }; return ( @@ -81,25 +164,18 @@ const AuthenticateJsx = ({navigation}) => { resizeMode = { "contain" } source = { require("assets/logo/logo-standalone.png") }/> - Account name + Instance domain name setState({ ...state, acct: value }) + value => setState({ ...state, instance: value }) }/> - Password - setState({ ...state, password: value }) - }/> + onPress = { _login }> Login