From 9c44029b5ab1b507d4f54b081dca7b3a9341deb3 Mon Sep 17 00:00:00 2001 From: zexigong <108034351+zexigong@users.noreply.github.com> Date: Mon, 29 May 2023 00:02:09 -0700 Subject: [PATCH 01/29] Oauth 2 --- src/Oauth.js | 284 ++++++++++++++++++ src/components/authentication/Login.vue | 44 +++ .../authentication/LoginSuccess.vue | 11 + src/components/landing_page/LandingPage.vue | 5 +- src/router/index.js | 6 + 5 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 src/Oauth.js create mode 100644 src/components/authentication/Login.vue create mode 100644 src/components/authentication/LoginSuccess.vue diff --git a/src/Oauth.js b/src/Oauth.js new file mode 100644 index 00000000..b6c541ac --- /dev/null +++ b/src/Oauth.js @@ -0,0 +1,284 @@ +// PKCE code Copyright (c) 2019 Aaron Parecki +// MIT Public License +// https://github.com/aaronpk/pkce-vanilla-js + +/*global $ Handlebars */ + +// Particular to this app. Change if you fork! +const getRandomValues = require('get-random-values') +const sha256 = require('crypto-js/sha256'); +const crypto = require('crypto'); + +module.exports = { + ajax, + startLogin, +} + +const server = 'https://apiclient.wiki/' +const clientID = "6ac53a07b581e30e47664cd9e8f3d0e4" + +// OAuth routes + +const authroot = `https://meta.wikimedia.org/w/rest.php` +const authorize = `${authroot}/oauth2/authorize` +const token = `${authroot}/oauth2/access_token` +const profileurl = `${authroot}/oauth2/resource/profile` + +console.log(10086); + +// API root + +const root = 'https://api.wikimedia.org/core/v1/wikipedia/en/' +const feeds = 'https://api.wikimedia.org/feed/v1/wikipedia/en/' + +function ajax(args) { + ensureToken(function(token) { + if (token) { + args.headers = (args.headers) ? args.headers : {} + args.headers['Authorization'] = `Bearer ${token}` + } + if (!args.error) { + args.error = function(jqXHR, textStatus, errorThrown) { + showError(`Error calling ${args.url}: ${errorThrown}`) + } + } + $.ajax(args) + }) +} + +const ensureToken = function(callback) { + let results = getLoginResults() + // Are we past the expiry date? + if (Date.now() > results.access_token_expired_ms) { + let pkce = loadPKCE() + let data = { + grant_type: "refresh_token", + refresh_token: results.refresh_token, + redirect_uri: `${server}callback`, + client_id: clientID, + code_verifier: pkce.codeVerifier + } + // We don't want to use access_token for this + $.post({ + url: token, + dataType: "json", + data: data, + success: function(newResults) { + saveLoginResults(newResults) + callback(newResults.access_token) + }, + error: function(jqXHR, textStatus, errorThrown) { + showError(`Error getting OAuth 2.0 refresh token: ${errorThrown}`) + } + }) + } else { + callback(results.access_token) + } +} + +var timer = null + +// from https://stackoverflow.com/questions/1267283/how-can-i-pad-a-value-with-leading-zeros + +const lp = function(number, width) { + width -= number.toString().length; + if ( width > 0 ) + { + return new Array( width + (/\./.test( number ) ? 2 : 1) ).join( '0' ) + number; + } + return number + ""; // always return a string +} + +function startLogin() { + let pkce = makePKCE() + savePKCE(pkce) + let params = { + client_id: clientID, + response_type: "code", + redirect_uri: `${server}callback`, + state: getPath(), + code_challenge: pkce.codeChallenge, + code_challenge_method: "S256" + } + let str = $.param(params) + let url = `${authorize}?${str}` + window.location = url + return false +} + +const endLogin = function() { + let query = (new URL(document.location)).searchParams + let state = query.get('state') + let error = query.get('error') + if (error) { + let errorDescription = query.get('error_description') + let message = query.get('message') + showError((message) ? message : ((errorDescription) ? errorDescription : error)) + if (state) { + routeTo(state) + } else { + routeTo('/') + } + return + } + let code = query.get('code') + let pkce = loadPKCE() + let data = { + grant_type: "authorization_code", + code: code, + redirect_uri: `${server}callback`, + client_id: clientID, + code_verifier: pkce.codeVerifier + } + // We don't want to use a token for this + $.post({ + url: token, + dataType: "json", + data: data, + success: function(results) { + saveLoginResults(results) + getProfile(function(profile) { + if (profile) { + saveProfile(profile) + } + resetNavbar() + routeTo(state) + }) + }, + error: function(xhr, status, text) { + showError(`Error finishing authorization: ${text}`) + } + }) +} + +const saveLoginResults = function(results) { + // TODO: save other important data + localStorage.setItem('access_token', results.access_token) + localStorage.setItem('refresh_token', results.refresh_token) + localStorage.setItem('access_token_expired_ms', (Date.now() + results.expires_in * 1000.0).toString()) +} + +const clearLoginResults = function() { + // TODO: save other important data + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + localStorage.removeItem('access_token_expired_ms') +} + +const getLoginResults = function() { + return { + access_token: localStorage.getItem('access_token'), + refresh_token: localStorage.getItem('refresh_token'), + access_token_expired_ms: parseFloat(localStorage.getItem('access_token_expired_ms')) + } +} + +const isLoggedIn = function() { + return getAccessToken() +} + +const getAccessToken = function() { + return localStorage.getItem('access_token') +} + +const getProfile = function(callback) { + ajax({ + url: profileurl, + success: function(profile) { + // workaround: this endpoint sometimes returns text/html instead of application/json + if (typeof(profile) == "string") { + profile = JSON.parse(profile) + } + callback(profile) + }, + error: function() { + // Just continue + callback(null) + } + }) +} + +const loadProfile = function() { + let profile = { + username: localStorage.getItem('username') + } + return profile +} + +const saveProfile = function(profile) { + localStorage.setItem('username', profile.username) +} + +const clearProfile = function() { + localStorage.removeItem('username') +} + +const getQuery = function() { + return (new URL(document.location)).searchParams +} + +const makePKCE = function() { + let pkce = { + state: generateRandomString(), + codeVerifier: generateRandomString() + } + pkce.codeChallenge = pkceChallengeFromVerifier(pkce.codeVerifier) + return pkce +} + +const savePKCE = function(pkce) { + if (typeof localStorage === "undefined" || localStorage === null) { + var LocalStorage = require('node-localstorage').LocalStorage; + localStorage = new LocalStorage('./scratch'); + } + localStorage.setItem("pkce_state", pkce.state) + localStorage.setItem("pkce_code_verifier", pkce.codeVerifier) +} + +const loadPKCE = function() { + let pkce = { + state: localStorage.getItem("pkce_state"), + codeVerifier: localStorage.getItem("pkce_code_verifier") + } + pkce.codeChallenge = pkceChallengeFromVerifier(pkce.codeVerifier) + return pkce +} + +const clearPKCE = function() { + localStorage.removeItem("pkce_state") + localStorage.removeItem("pkce_code_verifier") +} + +/* global Uint32Array */ +// changed to Unit8Array due to some errors. Do we need to fix it? + +const generateRandomString = function() { + var array = new Uint8Array(28) + getRandomValues(array) +// console.log(getRandomValues(array)) + return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('') +} + +// Base64-urlencodes the input string +function base64urlencode(str) { + return Buffer.from(str).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') +} + +/* global sha256 */ + +function pkceChallengeFromVerifier(v) { + let hash = crypto.createHash('sha256') + // let hash = sha256.create() + hash.update(v) + // let hashed = hash.array() + let stringed = String.fromCharCode.apply(null, hash) + let challenge = base64urlencode(stringed) + return challenge +} + +const logout = function () { + clearLoginResults() + clearProfile() + clearPKCE() +} + diff --git a/src/components/authentication/Login.vue b/src/components/authentication/Login.vue new file mode 100644 index 00000000..5c446f18 --- /dev/null +++ b/src/components/authentication/Login.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/authentication/LoginSuccess.vue b/src/components/authentication/LoginSuccess.vue new file mode 100644 index 00000000..40210797 --- /dev/null +++ b/src/components/authentication/LoginSuccess.vue @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/components/landing_page/LandingPage.vue b/src/components/landing_page/LandingPage.vue index 629f6d72..789dd9c1 100644 --- a/src/components/landing_page/LandingPage.vue +++ b/src/components/landing_page/LandingPage.vue @@ -1,6 +1,7 @@ diff --git a/src/components/authentication/LoginSuccess.vue b/src/components/authentication/LoginSuccess.vue index 40210797..6ffd3102 100644 --- a/src/components/authentication/LoginSuccess.vue +++ b/src/components/authentication/LoginSuccess.vue @@ -1,11 +1,53 @@ - - \ No newline at end of file + }, + mounted: async function() { + await this.getAccessToken(); + await this.getUserProfile(); + } +}; + From 2c6a5263e72078bac444ac5be62a4ddd019fb4b2 Mon Sep 17 00:00:00 2001 From: zexigong <108034351+zexigong@users.noreply.github.com> Date: Wed, 31 May 2023 19:57:51 -0700 Subject: [PATCH 03/29] Show username --- src/components/authentication/Login.vue | 4 +++- src/components/authentication/UserProfile.vue | 21 +++++++++++++++++++ src/components/landing_page/LandingPage.vue | 6 ++++-- src/components/menu/MainToolbar.vue | 5 ++++- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/components/authentication/UserProfile.vue diff --git a/src/components/authentication/Login.vue b/src/components/authentication/Login.vue index 3290cc8a..e903c3db 100644 --- a/src/components/authentication/Login.vue +++ b/src/components/authentication/Login.vue @@ -1,5 +1,8 @@ \ No newline at end of file diff --git a/src/components/landing_page/LandingPage.vue b/src/components/landing_page/LandingPage.vue index 789dd9c1..e2b44ceb 100644 --- a/src/components/landing_page/LandingPage.vue +++ b/src/components/landing_page/LandingPage.vue @@ -2,6 +2,7 @@
+
@@ -203,6 +204,7 @@ import TopicSearchBox from "@/components/TopicSearchBox"; import MainToolBar from "@/components/menu/MainToolbar"; import LoginButton from "@/components/authentication/Login"; + export default { name: "LandingPage", props: {}, @@ -229,8 +231,8 @@ export default { TopicSearchBox, // WikimapsWarperLayer, MainToolBar, - LoginButton - }, + LoginButton, +}, mounted: function() { this.$store.commit("resetState"); // this.$store.commit('setSelectedBasemap', this.mapOfTheDay); diff --git a/src/components/menu/MainToolbar.vue b/src/components/menu/MainToolbar.vue index 192d713b..552b6799 100644 --- a/src/components/menu/MainToolbar.vue +++ b/src/components/menu/MainToolbar.vue @@ -12,6 +12,7 @@ {{ landingPageName }} +
@@ -19,6 +20,7 @@ \ No newline at end of file diff --git a/src/components/landing_page/LandingPage.vue b/src/components/landing_page/LandingPage.vue index e2b44ceb..28a6d749 100644 --- a/src/components/landing_page/LandingPage.vue +++ b/src/components/landing_page/LandingPage.vue @@ -1,7 +1,6 @@ @@ -20,6 +21,7 @@ + + + + diff --git a/src/components/authentication/UserProfile.vue b/src/components/authentication/UserProfile.vue index 04c0b7c8..999cec1e 100644 --- a/src/components/authentication/UserProfile.vue +++ b/src/components/authentication/UserProfile.vue @@ -2,7 +2,7 @@ - + + \ No newline at end of file diff --git a/src/components/upload/Popup.vue b/src/components/upload/Popup.vue index 4525b874..dcfa9e10 100644 --- a/src/components/upload/Popup.vue +++ b/src/components/upload/Popup.vue @@ -1,7 +1,36 @@ @@ -231,6 +387,7 @@ export default{ .popup-inner{ background: #FFF; padding:30px; + max-width: 1000px; } .grid-icons { diff --git a/src/store/translation/en.json b/src/store/translation/en.json index 018ce1a5..2185f197 100644 --- a/src/store/translation/en.json +++ b/src/store/translation/en.json @@ -527,7 +527,10 @@ "filename": "File name", "upload": "Upload" , "closePopup": "Close", - "cannotUpload": "This image cannot be uploaded to Wikimedia Commons because the license is not in the list of allowed licenses. If you think this is an error, please [file an issue] on " + "cannotUpload": "This image cannot be uploaded to Wikimedia Commons because the license is not in the list of allowed licenses. If you think this is an error, please [file an issue] on ", + "uploadInProgress": "Upload in progress", + "cancel": "cancel", + "category": "Category" } }, From 429893a3120d45f7c6ab473b6b09c12756416d2d Mon Sep 17 00:00:00 2001 From: zexigong <108034351+zexigong@users.noreply.github.com> Date: Sun, 6 Aug 2023 23:59:08 -0700 Subject: [PATCH 12/29] new Oauth app support upload and edit --- src/components/authentication/Login.vue | 10 +++------- src/components/authentication/LoginSuccess.vue | 6 ++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/authentication/Login.vue b/src/components/authentication/Login.vue index cf2bfe8c..690e86db 100644 --- a/src/components/authentication/Login.vue +++ b/src/components/authentication/Login.vue @@ -16,13 +16,9 @@ export default { name: 'LoginButton', data(){ return{ - // This is a public app with upload grant - CLIENT_ID : "fc15fabf0c4d39e070084bb2d5accd5e", - CLIENT_SECRET:"43a72c51df15d4219dd7ec59f174a673cc37be8f" - - // This is a public app without upload grant - // CLIENT_ID : "c0860cd2f1a6e322e1b4f20597c2be7a", - // CLIENT_SECRET:"0b08788ef5274e77a8b4454422cb1866d2f6a30a" + // This is a public app with upload and edit grant + CLIENT_ID : "f2aa70edfeb48a0eb08614c69b9148b4", + CLIENT_SECRET:"48830e519cfc29240c9291a7f301e437d0355958" } }, components: { diff --git a/src/components/authentication/LoginSuccess.vue b/src/components/authentication/LoginSuccess.vue index 703296b0..872d1ee5 100644 --- a/src/components/authentication/LoginSuccess.vue +++ b/src/components/authentication/LoginSuccess.vue @@ -24,10 +24,8 @@ export default { name: "LoginSuccess", data() { return { - CLIENT_ID : "fc15fabf0c4d39e070084bb2d5accd5e", - CLIENT_SECRET:"43a72c51df15d4219dd7ec59f174a673cc37be8f", - // CLIENT_ID: "c0860cd2f1a6e322e1b4f20597c2be7a", - // CLIENT_SECRET: "0b08788ef5274e77a8b4454422cb1866d2f6a30a", + CLIENT_ID : "f2aa70edfeb48a0eb08614c69b9148b4", + CLIENT_SECRET:"48830e519cfc29240c9291a7f301e437d0355958", profileUrl: "https://meta.wikimedia.org/w/rest.php/oauth2/resource/profile" }; From e1cacebfc041f798261195cd099ef40fc21ad513 Mon Sep 17 00:00:00 2001 From: zexigong <108034351+zexigong@users.noreply.github.com> Date: Wed, 23 Aug 2023 00:26:09 -0700 Subject: [PATCH 13/29] update popup --- src/components/upload/Popup.vue | 752 ++++++++++++++++---------------- src/store/translation/en.json | 7 +- 2 files changed, 393 insertions(+), 366 deletions(-) diff --git a/src/components/upload/Popup.vue b/src/components/upload/Popup.vue index dcfa9e10..4f87ffd7 100644 --- a/src/components/upload/Popup.vue +++ b/src/components/upload/Popup.vue @@ -1,399 +1,423 @@ \ No newline at end of file + diff --git a/src/store/translation/en.json b/src/store/translation/en.json index 2185f197..795b8bd1 100644 --- a/src/store/translation/en.json +++ b/src/store/translation/en.json @@ -530,8 +530,11 @@ "cannotUpload": "This image cannot be uploaded to Wikimedia Commons because the license is not in the list of allowed licenses. If you think this is an error, please [file an issue] on ", "uploadInProgress": "Upload in progress", "cancel": "cancel", - "category": "Category" - + "category": "Category", + "uploadSuccess": "uploadSuccess", + "view": "View", + "inWikimediaCommons": "in Wikimedia Commons", + "ok": "OK" } }, "general": { From 2c597ca601863cdc1624d5f2fec675fb3e743c6a Mon Sep 17 00:00:00 2001 From: zexigong <108034351+zexigong@users.noreply.github.com> Date: Sun, 27 Aug 2023 22:51:47 -0700 Subject: [PATCH 14/29] add comment and item pull down --- src/components/upload/InUpload.vue | 5 +- src/components/upload/ItemPullDown.vue | 329 +++++++++++++++++++++++++ src/components/upload/Popup.vue | 14 +- 3 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 src/components/upload/ItemPullDown.vue diff --git a/src/components/upload/InUpload.vue b/src/components/upload/InUpload.vue index 3cbc9d56..2967cce2 100644 --- a/src/components/upload/InUpload.vue +++ b/src/components/upload/InUpload.vue @@ -1,3 +1,6 @@ + - + \ No newline at end of file diff --git a/src/components/upload/Popup.vue b/src/components/upload/Popup.vue index 4f87ffd7..d809dcf1 100644 --- a/src/components/upload/Popup.vue +++ b/src/components/upload/Popup.vue @@ -1,6 +1,11 @@ + diff --git a/src/store/translation/en.json b/src/store/translation/en.json index a89b17bd..8b9cea88 100644 --- a/src/store/translation/en.json +++ b/src/store/translation/en.json @@ -275,7 +275,7 @@ "doGeolocatingText": "Set the location...", "showImageText": "Show image", "selectFeatured": "Choose as a header image", - "upload" : "upload" + "upload" : "Upload to Commons" }, "imagesRemoveMenu": { "menuTitle": "Remove the image because...", @@ -523,29 +523,29 @@ "upload": { "popup": { "popupTitle": "Upload image", - "popupTip": "Verify the information and edit the texts if needed.", + "intro": "Verify the information and edit if needed.", "filename": "File name", - "upload": "Upload" , + "upload": "Upload image" , "closePopup": "Close", - "cannotUpload": "This image cannot be uploaded to Wikimedia Commons because the license is not in the list of allowed licenses. If you think this is an error, please [file an issue] on ", + "cannotUpload": "This image cannot be uploaded to Wikimedia Commons because the license is not in the list of allowed licenses. If you think this is an error, please file an issue on ", "uploadInProgress": "Upload in progress", - "cancel": "cancel", + "cancel": "Cancel", "category": "Category", - "uploadSuccess": "uploadSuccess", + "uploadSuccess": "Upload successful.", "view": "View", "inWikimediaCommons": "in Wikimedia Commons", "ok": "OK", "Github": "Github", - "topicPage": "go to topic page" + "topicPage": "View topic" } }, "login": { "loginMenu": { - "title": "User Profile", - "tooltip": "Login", - "item": "Log In", - "logout": "Log Out", - "showProfile": "Show profile" + "title": "User profile", + "tooltip": "Log in", + "item": "Log in", + "logout": "Log out", + "showProfile": "Log out" }, "loginSuccess": "Login successful. You are being redirected back to Wikidocumentaries."