diff --git a/.env b/.env new file mode 100644 index 0000000..1cf9d80 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +VUE_APP_CLIENT_ID=q2w45itLPXzlApDZyLgsoDGLO3HAArySZeQaQt40 +VUE_APP_REDIRECT_URI=https://readerbench.com/authorized +VUE_APP_READERBENCH_API_BASE_URL=https://readerbench.com/api/v2 +VUE_APP_READERBENCH_API_LOGIN_URL=${VUE_APP_READERBENCH_API_BASE_URL}/accounts/login/?client_id=${VUE_APP_CLIENT_ID}&redirect_uri=${VUE_APP_REDIRECT_URI} +VUE_APP_READERBENCH_API_LOGOUT_URL=${VUE_APP_READERBENCH_API_BASE_URL}/accounts/logout +VUE_APP_READERBENCH_API_SIGNUP_URL=${VUE_APP_READERBENCH_API_BASE_URL}/accounts/signup/?client_id=${VUE_APP_CLIENT_ID}&redirect_uri=${VUE_APP_REDIRECT_URI} +VUE_APP_READERBENCH_API_TOKEN_ENDPOINT=/oauth2/token/ +VUE_APP_READERBENCH_API_USER_DETAILS_ENDPOINT=/users/me \ No newline at end of file diff --git a/README.md b/README.md index 244c80d..81a80bd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # readerbench-vue +### Local development + +``` +Please create a **.env.development.local** or **.env.local** file and overwrite the variables from .env to suit the local environment +``` + ## Project setup ``` npm install @@ -31,4 +37,4 @@ npm run lint ``` ### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). +See [Configuration Reference](https://cli.vuejs.org/config/). \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 731d028..a286713 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "core-js": "^3.8.3", "dotenv": "^16.0.3", "echarts": "^5.4.0", + "js-sha256": "^0.11.0", "json-as-xlsx": "^2.5.4", "lodash": "^4.17.21", "primeicons": "^6.0.1", @@ -10609,6 +10610,11 @@ "node": ">=0.6.0" } }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -18510,4 +18516,4 @@ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a6425e8..008eb2b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "core-js": "^3.8.3", "dotenv": "^16.0.3", "echarts": "^5.4.0", + "js-sha256": "^0.11.0", "json-as-xlsx": "^2.5.4", "lodash": "^4.17.21", "primeicons": "^6.0.1", diff --git a/public/index.html b/public/index.html index 28fedb8..8b0f267 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,7 @@ - + ReaderBench diff --git a/src/components/partials/Nav.vue b/src/components/partials/Nav.vue index 511ec6e..4d3a6ca 100644 --- a/src/components/partials/Nav.vue +++ b/src/components/partials/Nav.vue @@ -1,274 +1,392 @@ diff --git a/src/router/index.ts b/src/router/index.ts index 7d4137d..e7ccf28 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -12,7 +12,10 @@ const routes: Array = [ { path: '/models', name: 'models', - component: () => import('../views/ModelsView.vue') + component: () => import('../views/ModelsView.vue'), + meta: { + requiresAuth: true, + }, }, { path: '/services', @@ -25,14 +28,9 @@ const routes: Array = [ component: () => import('../views/People.vue') }, { - path: '/login', - name: 'login', - component: () => import('@/views/Login.vue') - }, - { - path: '/sign-up', - name: 'sign-up', - component: () => import('@/views/SignUp.vue') + path: '/authorized', + name: 'authorized', + component: () => import('@/views/Authorized.vue') }, { path: '/projects', @@ -69,21 +67,33 @@ const routes: Array = [ path: '/datasets', name: 'datasets', component: () => import('@/views/DatasetsView.vue'), + meta: { + requiresAuth: true, + } }, { path: '/datasets/:dataset_id', name: 'dataset', - component: () => import('@/views/DatasetView.vue') + component: () => import('@/views/DatasetView.vue'), + meta: { + requiresAuth: true, + }, }, { path: '/processing-queue', name: 'processingqueue', component: () => import('@/views/ProcessingQueueView.vue'), + meta: { + requiresAuth: true, + }, }, { path: '/profile', name: 'userprofileView', component: () => import('@/views/UserProfileView.vue'), + meta: { + requiresAuth: true, + }, }, { path: '/services/stt', @@ -105,6 +115,9 @@ const routes: Array = [ path: '/models/:id/prediction', name: 'modelpredictionview', component: () => import('@/views/ModelPredictionView.vue'), + meta: { + requiresAuth: true, + }, } ] @@ -115,13 +128,13 @@ const router = createRouter({ router.beforeEach((to, from, next) => { const route: RouteRecordNormalized = first(to.matched); - if (!isNil(route)) { if (route.meta.requiresAuth && !auth.isAuthenticated()) { const location: RouteLocationRaw = { - name: 'login', + name: 'home', query: { redirect: encodeURIComponent(to.fullPath), + sessionExpired: 'true' } }; return next(location); diff --git a/src/services/auth.ts b/src/services/auth.ts index 9c45761..f641667 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,125 +1,169 @@ -import { isNil } from 'lodash'; -import router from '@/router'; -import axios from 'axios'; +import { isNil } from "lodash"; +import router from "@/router"; +import axios from "axios"; +import { sha256 } from "js-sha256"; interface Callback { (message: any): void; } export default { - isAuthenticated() { - const user = localStorage.getItem('user'); - const token = localStorage.getItem('token'); - - return !isNil(user) && !isNil(token); + const token = localStorage.getItem("token"); + + return !isNil(token); }, - // Only for testing - dummy_successful_login(creds, redirect: string, onSuccess: Callback, onError: Callback) { - const user = { - grant_type: "password", - client_id: "8okRYORQww9VK0x3UTHAe8rl0dDvCUL6s3d6T43z", - client_secret: "sOyagG4VyhuBuVjrxzPtO848IbLSk19q7xGKp83c7FDNnNRGJRKOX3RAFI0F4SQMNtIx2Gg9xQTxPPKaTMikniMSbci7nppISLKq8EoRB5U8RZslOPu85AB4H5hI5EsZ", - username: creds.username, - password: creds.password - }; + get_user_details() { + const apiUserDetailsPath = + process.env.VUE_APP_READERBENCH_API_USER_DETAILS_ENDPOINT; - if (redirect) { - router.push({ path: redirect, hash: '#logged_in' }) + if (!apiUserDetailsPath) { + throw new Error( + "API User details path is not defined in environment variables" + ); } - localStorage.setItem('user', JSON.stringify(user)); - localStorage.setItem('token', 'secret_token'); - localStorage.setItem('refresh_token', 'secret_refresh_token'); - onSuccess(user); + return axios.get(apiUserDetailsPath); }, - login(creds, redirect: string, onSuccess: Callback, onError: Callback) { - - const user = { - grant_type: "password", - client_id: "8XE5YdZbhRdcLLovx3LxxhUQbKl2QPO0rvGMpWHy", - client_secret: "tybRzdzdcPykS7z358UqEMZApKJc6V36YwLBWvswHohVwFATJWmELCrXOs6zVQkEAChNlP87s1aQDxzkqu8O00PBQ1Qxv8vEf5XvGdxnneryOqxQBPri295Zve0fw42J", - username: creds.username, - password: creds.password + get_access_token( + creds, + redirect: string, + onSuccess: Callback, + onError: Callback + ) { + const code_verifier = localStorage.getItem("code_verifier"); + localStorage.removeItem("code_verifier"); + const payload = { + grant_type: "authorization_code", + client_id: process.env.VUE_APP_CLIENT_ID, + code: creds.code, + redirect_uri: creds.redirect_uri, + code_verifier: code_verifier }; + const apiTokenPath = process.env.VUE_APP_READERBENCH_API_TOKEN_ENDPOINT; + + if (!apiTokenPath) { + throw new Error("API Token path is not defined in environment variables"); + } + axios - .post('/oauth2/token/', user) - .then(response => { - if (redirect) { - router.push({ path: redirect, hash: '#logged_in' }) - } - localStorage.setItem('user', JSON.stringify(user)); - localStorage.setItem('token', response.data.access_token); - localStorage.setItem('refresh_token', response.data.refresh_token); - localStorage.setItem('token_valid_until_timestamp', String(response.data.expires_in + (Math.floor(Date.now() / 1000)))); - - onSuccess(user.username); - }) - .catch(error => { - onError(error); - }) + .post(apiTokenPath, payload) + .then((response) => { + if (redirect) { + router.push({ path: redirect, hash: "#logged_in" }); + } + localStorage.setItem("token", response.data.access_token); + localStorage.setItem("refresh_token", response.data.refresh_token); + localStorage.setItem( + "token_valid_until_timestamp", + String(response.data.expires_in + Math.floor(Date.now() / 1000)) + ); + + try { + const call_user_details = this.get_user_details() + + call_user_details.then((response) => { + onSuccess(response.data.username); + }) + .catch((error) => { + onError(error); + }); + } catch (error) { + onError({response: {data: {error_description: error}}}) + } + }) + .catch((error) => { + onError(error); + }); }, refresh_access_token(onSuccess: Callback, onError: Callback) { const payload = { grant_type: "refresh_token", - client_id: "8XE5YdZbhRdcLLovx3LxxhUQbKl2QPO0rvGMpWHy", - client_secret: "tybRzdzdcPykS7z358UqEMZApKJc6V36YwLBWvswHohVwFATJWmELCrXOs6zVQkEAChNlP87s1aQDxzkqu8O00PBQ1Qxv8vEf5XvGdxnneryOqxQBPri295Zve0fw42J", - refresh_token: localStorage.getItem('refresh_token') + client_id: process.env.VUE_APP_CLIENT_ID, + refresh_token: localStorage.getItem("refresh_token"), }; + const apiTokenPath = process.env.VUE_APP_READERBENCH_API_TOKEN_ENDPOINT; + + if (!apiTokenPath) { + throw new Error("API Token path is not defined in environment variables"); + } + axios - .post('/oauth2/token/', payload) - .then(response => { - - localStorage.setItem('token', response.data.access_token); - if (response.data.refresh_token != undefined) - localStorage.setItem('refresh_token', response.data.refresh_token); - localStorage.setItem('token_valid_until_timestamp', String(response.data.expires_in + (Math.floor(Date.now() / 1000)))); - - onSuccess(response); - }) - .catch(error => { - onError(error); - }) + .post(apiTokenPath, payload) + .then((response) => { + localStorage.setItem("token", response.data.access_token); + if (response.data.refresh_token != undefined) + localStorage.setItem("refresh_token", response.data.refresh_token); + localStorage.setItem( + "token_valid_until_timestamp", + String(response.data.expires_in + Math.floor(Date.now() / 1000)) + ); + + onSuccess(response); + }) + .catch((error) => { + onError(error); + }); }, - signUp(details, onSuccess: Callback, onError: Callback) { + login(window) { + // Generate code verifier + const code_verifier = Array.from({ length: 128 }, () => + String.fromCharCode(Math.floor(Math.random() * 95) + 32) + ).join(""); + localStorage.setItem("code_verifier", code_verifier); + + // string of 64 hex characters -> 32 bytes -> 32 ascii characters + const code_verifier_hashed = sha256(code_verifier); + + // Because the code verifier is a string of 64 hex characters, + // we need to convert it to a string of the corresponding ascii characters + // because btoa expects a string of ascii characters. Otherwise it will treat a hex character as an ascii character. + let code_verifier_hashed_ascii = ""; + for (let i = 0; i < code_verifier_hashed.length; i += 2) { + const ascii_char = String.fromCharCode(parseInt(code_verifier_hashed.substring(i, i + 2), 16)); + code_verifier_hashed_ascii += ascii_char; + } + const code_verifier_hashed_base64 = btoa(code_verifier_hashed_ascii); - const user = { - email: details.email, - password: details.password, - first_name: details.firstName, - last_name: details.lastName, - }; + // what this does is: + // 1. remove the padding characters '=' + // 2. replace '+' with '-' + // 3. replace '/' with '_' + // this is because the base64url encoding is a variant of base64 encoding that uses URL and filename safe alphabet + const code_challenge = code_verifier_hashed_base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); - axios - .post('/users/register', user) - .then(response => { - onSuccess(response); - }) - .catch(error => { - onError(error); - }) + // Redirect to the login page with the code challenge and the method used to generate it + window.location.href = process.env.VUE_APP_READERBENCH_API_LOGIN_URL + `&code_challenge=${code_challenge}` + `&code_challenge_method=S256`; }, - // To log out logout: function (callback?: Callback) { - this.clearLocalStorage(); - router.push({ path: '/', hash: '#logged_out' }) + const apiLogoutUrl = process.env.VUE_APP_READERBENCH_API_LOGOUT_URL; - if (callback) { - callback("You have been logged out"); + if (!apiLogoutUrl) { + throw new Error( + "API Logout path is not defined in environment variables" + ); } + + axios.get(apiLogoutUrl, { withCredentials: true }).then(() => { + this.clearLocalStorage(); + router.push({ path: "/", hash: "#logged_out" }); + + if (callback) { + callback("You have been logged out"); + } + }); }, clearLocalStorage() { - localStorage.removeItem("user"); localStorage.removeItem("token"); localStorage.removeItem("token_valid_until_timestamp"); localStorage.removeItem("refresh_token"); - } -} + }, +}; diff --git a/src/services/http-interceptor.ts b/src/services/http-interceptor.ts index b0f3215..715ce58 100644 --- a/src/services/http-interceptor.ts +++ b/src/services/http-interceptor.ts @@ -4,8 +4,7 @@ import router from "@/router"; export function httpInterceptor() { axios.interceptors.request.use(async (request) => { - // TODO: read from env variable - request.baseURL = "https://readerbench.com/api/v2"; + request.baseURL = `${process.env.VUE_APP_READERBENCH_API_BASE_URL}`; request.headers = request.headers ?? {}; if (request.data) { @@ -13,7 +12,7 @@ export function httpInterceptor() { return request; } - if (request.data.grant_type && request.data.grant_type == "password") { + if (request.data.grant_type && request.data.grant_type == "authorization_code") { return request; } } @@ -44,38 +43,11 @@ export function httpInterceptor() { }); } catch (error) { auth.clearLocalStorage(); - - // Redirect to login page - router.push({ path: "/login", hash: "#session_expired" }); - + router.push({ path: "/", hash: "#session_expired" }); return Promise.reject("Your session has expired. Please log in again to continue."); } } return request; }); - - //TODO: Could cause problems if 401 status is not emitted due to using an invalid token -> to check - // axios.interceptors.response.use( - // (response) => { - // return response; - // }, - // (error) => { - // if (error.response) { - // const status = error.response.status; - - // if (status === 401) { - // // Handle error due to invalid token usage - // auth.clearLocalStorage(); - - // // Redirect to login page - // router.push({ path: "/login", hash: "#session_expired" }); - - // return Promise.reject("Your session has expired. Please log in again to continue."); - // } - // } - - // return Promise.reject(error); - // } - // ); } diff --git a/src/views/Authorized.vue b/src/views/Authorized.vue new file mode 100644 index 0000000..4e4a2de --- /dev/null +++ b/src/views/Authorized.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 7c514ab..8b37fd4 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,153 +1,273 @@ diff --git a/src/views/Login.vue b/src/views/Login.vue deleted file mode 100644 index 84b7837..0000000 --- a/src/views/Login.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - diff --git a/src/views/SignUp.vue b/src/views/SignUp.vue deleted file mode 100644 index ec2a981..0000000 --- a/src/views/SignUp.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - diff --git a/src/views/TextAnalysisView.vue b/src/views/TextAnalysisView.vue index 1fac6c2..40c6b0f 100644 --- a/src/views/TextAnalysisView.vue +++ b/src/views/TextAnalysisView.vue @@ -123,7 +123,7 @@ export default { { id: 4, name: 'Keyword Extraction', - languages: [3,8], + languages: [1, 3, 8], process: this.taService.getKeywords, payloadTemplate: { text: null,