diff --git a/config/prod.env.js b/config/prod.env.js index 15a0fa36..132ef011 100644 --- a/config/prod.env.js +++ b/config/prod.env.js @@ -2,5 +2,7 @@ process = require('process'); module.exports = { NODE_ENV: '"production"', - API_URL: JSON.stringify(process.env.API_URL) + API_URL: JSON.stringify(process.env.API_URL), + OAUTH_CLIENT_ID: JSON.stringify(process.env.OAUTH_CLIENT_ID), + OAUTH_CLIENT_SECRET: JSON.stringify(process.env.OAUTH_CLIENT_SECRET), }; diff --git a/src/App.vue b/src/App.vue index 7870828b..f0a0c566 100644 --- a/src/App.vue +++ b/src/App.vue @@ -169,7 +169,7 @@ a:hover { .toolbar { display: flex; color: #333; - padding: 0 20px; + padding-left: 20px; min-height: 45px; /* Normal height, but line wrap can grow this */ align-items: flex-start; /* We want multiple lines to align to the top */ line-height: 1.2; @@ -187,8 +187,8 @@ a:hover { justify-content: center; cursor: pointer; width: 45px; - padding-top: 9px; /* To adjust baseline to a good position */ - padding-bottom: 6px; /* For visual symmetry */ + height: 45px; + align-items: center; } .toolbar-item-a { @@ -717,6 +717,19 @@ i.wikiglyph { -webkit-column-break-inside: avoid; } +.upload-button { + padding: 10px 15px; + background-color: var(--main-red); + color: white; + font-weight: 600; + border-radius: 3px; + white-space: nowrap; +} + +a:hover .upload-button { + background-color: var(--main-orange); +} + .data-select { display: block; background: var(--main-red); @@ -729,6 +742,16 @@ i.wikiglyph { /* transition: background 80ms ease-in, color 80ms ease-in; */ } +.data-select a { + color:white; + font-size: 0.7em; +} + +.data-select a:hover { + box-shadow: none; + text-decoration: underline; +} + .data-button { display: inline-block; position: absolute; diff --git a/src/components/ImageGrid.vue b/src/components/ImageGrid.vue index 93bfde5d..102b9e0e 100644 --- a/src/components/ImageGrid.vue +++ b/src/components/ImageGrid.vue @@ -18,9 +18,9 @@
{{ getCredits(item) }}
-
-
- +
+
+
- +
@@ -48,8 +48,8 @@ diff --git a/src/components/authentication/LoginSuccess.vue b/src/components/authentication/LoginSuccess.vue new file mode 100644 index 00000000..71f3d8af --- /dev/null +++ b/src/components/authentication/LoginSuccess.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/src/components/authentication/UserProfile.vue b/src/components/authentication/UserProfile.vue new file mode 100644 index 00000000..c772fb87 --- /dev/null +++ b/src/components/authentication/UserProfile.vue @@ -0,0 +1,46 @@ + + + + + \ No newline at end of file diff --git a/src/components/image_viewer/ImageViewer.vue b/src/components/image_viewer/ImageViewer.vue index 4ee0a05a..d9ce8ba7 100644 --- a/src/components/image_viewer/ImageViewer.vue +++ b/src/components/image_viewer/ImageViewer.vue @@ -38,6 +38,7 @@ >
{{ $t('general.menus.actionMenuTitle') }}
+
+
@@ -201,6 +202,7 @@ import TopicSearchBox from "@/components/TopicSearchBox"; import MainToolBar from "@/components/menu/MainToolbar"; + export default { name: "LandingPage", props: {}, @@ -226,8 +228,8 @@ export default { components: { TopicSearchBox, // WikimapsWarperLayer, - MainToolBar - }, + MainToolBar, +}, mounted: function() { this.$store.commit("resetState"); // this.$store.commit('setSelectedBasemap', this.mapOfTheDay); diff --git a/src/components/menu/ImagesActionMenu.vue b/src/components/menu/ImagesActionMenu.vue index 81ff8cd8..3bb33a0e 100644 --- a/src/components/menu/ImagesActionMenu.vue +++ b/src/components/menu/ImagesActionMenu.vue @@ -2,11 +2,12 @@
{{ $t('topic_page.TopicImages.imagesActionMenu.menuTitle') }}
+
@@ -14,11 +15,14 @@ diff --git a/src/components/menu/MainToolbar.vue b/src/components/menu/MainToolbar.vue index 192d713b..0ed7731f 100644 --- a/src/components/menu/MainToolbar.vue +++ b/src/components/menu/MainToolbar.vue @@ -12,6 +12,8 @@ {{ landingPageName }} + +
@@ -19,17 +21,22 @@ + + + + \ No newline at end of file diff --git a/src/components/upload/ItemPullDown.vue b/src/components/upload/ItemPullDown.vue new file mode 100644 index 00000000..3d8e21b2 --- /dev/null +++ b/src/components/upload/ItemPullDown.vue @@ -0,0 +1,339 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/upload/Popup.vue b/src/components/upload/Popup.vue new file mode 100644 index 00000000..fe679add --- /dev/null +++ b/src/components/upload/Popup.vue @@ -0,0 +1,581 @@ + + + + diff --git a/src/pkce-challenge.js b/src/pkce-challenge.js new file mode 100644 index 00000000..3327ab5d --- /dev/null +++ b/src/pkce-challenge.js @@ -0,0 +1,99 @@ +// Copy of pkce-challenge 4.0.1 from NPM for use in OAuth 2 login +// The package as installed didn't work in our build system + +/* +MIT License + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +const crypto = globalThis.crypto; // WDX: modified to only work on browsers + +/** + * Creates an array of length `size` of random bytes + * @param size + * @returns Array of random ints (0 to 255) + */ +function getRandomValues(size) { + return crypto.getRandomValues(new Uint8Array(size)); +} +/** Generate cryptographically strong random string + * @param size The desired length of the string + * @returns The random string + */ +function random(size) { + const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; + let result = ""; + const randomUints = getRandomValues(size); + for (let i = 0; i < size; i++) { + // cap the value of the randomIndex to mask.length - 1 + const randomIndex = randomUints[i] % mask.length; + result += mask[randomIndex]; + } + return result; +} +/** Generate a PKCE challenge verifier + * @param length Length of the verifier + * @returns A random verifier `length` characters long + */ +function generateVerifier(length) { + return random(length); +} +/** Generate a PKCE code challenge from a code verifier + * @param code_verifier + * @returns The base64 url encoded code challenge + */ +export async function generateChallenge(code_verifier) { + const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(code_verifier)); + // Generate base64url string + // btoa is deprecated in Node.js but is used here for web browser compatibility + // (which has no good replacement yet, see also https://github.com/whatwg/html/issues/6811) + return btoa(String.fromCharCode(...new Uint8Array(buffer))) + .replace(/\//g, '_') + .replace(/\+/g, '-') + .replace(/=/g, ''); +} +/** Generate a PKCE challenge pair + * @param length Length of the verifer (between 43-128). Defaults to 43. + * @returns PKCE challenge pair + */ +export default async function pkceChallenge(length) { + if (!length) + length = 43; + if (length < 43 || length > 128) { + throw `Expected a length between 43 and 128. Received ${length}.`; + } + const verifier = generateVerifier(length); + const challenge = await generateChallenge(verifier); + return { + code_verifier: verifier, + code_challenge: challenge, + }; +} +/** Verify that a code_verifier produces the expected code challenge + * @param code_verifier + * @param expectedChallenge The code challenge to verify + * @returns True if challenges are equal. False otherwise. + */ +export async function verifyChallenge(code_verifier, expectedChallenge) { + const actualChallenge = await generateChallenge(code_verifier); + return actualChallenge === expectedChallenge; +} diff --git a/src/router/index.js b/src/router/index.js index da76f8b9..5aa1dfba 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -13,6 +13,7 @@ import 'ol/ol.css' import MainPage from '@/components/MainPage' import LandingPage from '@/components/landing_page/LandingPage' import devpage from '@/components/landing_page/Developer' +import LoginSuccess from '@/components/authentication/LoginSuccess' Vue.use(Router) Vue.use(VueMasonryPlugin) @@ -37,6 +38,11 @@ export default new Router({ name: 'LandingPage', component: LandingPage }, + { + path: '/logged-in', + name: 'LoginSuccess', + component: LoginSuccess + }, { path: '/wikipedia/:language/:topic', component: MainPage diff --git a/src/store/translation/en.json b/src/store/translation/en.json index 02b687b9..8b9cea88 100644 --- a/src/store/translation/en.json +++ b/src/store/translation/en.json @@ -274,7 +274,8 @@ "menuTooltip": "Actions", "doGeolocatingText": "Set the location...", "showImageText": "Show image", - "selectFeatured": "Choose as a header image" + "selectFeatured": "Choose as a header image", + "upload" : "Upload to Commons" }, "imagesRemoveMenu": { "menuTitle": "Remove the image because...", @@ -519,6 +520,36 @@ "optionList": "List" } }, + "upload": { + "popup": { + "popupTitle": "Upload image", + "intro": "Verify the information and edit if needed.", + "filename": "File name", + "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 ", + "uploadInProgress": "Upload in progress", + "cancel": "Cancel", + "category": "Category", + "uploadSuccess": "Upload successful.", + "view": "View", + "inWikimediaCommons": "in Wikimedia Commons", + "ok": "OK", + "Github": "Github", + "topicPage": "View topic" + } + }, + "login": { + "loginMenu": { + "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." + }, "general": { "wikidocumentaries": "Wikidocumentaries", "ok": "OK",