diff --git a/.env.development b/.env.development index e69de29..1a75d96 100644 --- a/.env.development +++ b/.env.development @@ -0,0 +1 @@ +VUE_APP_API_LOCATION=http://localhost:5000/ \ No newline at end of file diff --git a/.env.production b/.env.production index e69de29..b5f5dec 100644 --- a/.env.production +++ b/.env.production @@ -0,0 +1 @@ +VUE_APP_API_LOCATION=https://spoton.k8s.kmlabz.com/api/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5628576..82f9760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1105,6 +1105,61 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@nuxt/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-XG7rUdXG9fcafu9KTDIYjJSkRO38EwjlKYIb5TQ/0WDbiTUTtUtgncMscKOYzfsY86kGs05pAuMOR+3Fi0aN3A==", + "requires": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "@soda/friendly-errors-webpack-plugin": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz", @@ -2472,6 +2527,14 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -2696,6 +2759,23 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "bootstrap": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", + "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==" + }, + "bootstrap-vue": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.19.0.tgz", + "integrity": "sha512-IjAXUSrRU5Qu9x3uwUcoj6LtysKbCVeWoJOsODyI/WokStUr95M+tTIajXUjIrB/Nsk0fS+RNvZnm2sWeNFrhg==", + "requires": { + "@nuxt/opencollective": "^0.3.2", + "bootstrap": ">=4.5.3 <5.0.0", + "popper.js": "^1.16.1", + "portal-vue": "^2.1.7", + "vue-functional-data-merge": "^3.1.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3545,6 +3625,11 @@ "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, + "consola": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz", + "integrity": "sha512-vlcSGgdYS26mPf7qNi+dCisbhiyDnrN1zaRbw3CSuc2wGOMEGGPsp46PdRG5gqXwgtJfjxDkxRNAgRPr1B77vQ==" + }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -5435,8 +5520,7 @@ "follow-redirects": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", - "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", - "dev": true + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" }, "for-in": { "version": "1.0.2", @@ -7440,6 +7524,11 @@ "lower-case": "^1.1.1" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -8105,6 +8194,16 @@ "ts-pnp": "^1.1.6" } }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, + "portal-vue": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz", + "integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==" + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -10861,6 +10960,11 @@ } } }, + "vue-functional-data-merge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz", + "integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA==" + }, "vue-hot-reload-api": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", diff --git a/package.json b/package.json index faf1ad7..2aba88c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.21.0", + "bootstrap": "^4.5.3", + "bootstrap-vue": "^2.19.0", "core-js": "^3.6.5", "vue": "^2.6.11", "vue-router": "^3.2.0", diff --git a/src/App.vue b/src/App.vue index 6c26aa6..db985a1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,32 +1,62 @@ + + diff --git a/src/api/index.js b/src/api/index.js index e69de29..5372d34 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -0,0 +1,134 @@ +import axios from 'axios' + +const API_BASE_URL = process.env.VUE_APP_API_LOCATION +const LOCAL_STORAGE_KEY = "JWT" + +const COMMON_ERROR_CODES = { + 403: "Access Denied", + 401: "Authentication failed", + 400: "Invalid Request", + 417: "Invalid Request" +} + +export default new class { + + _setupHTTPObject() { + const token = localStorage.getItem(LOCAL_STORAGE_KEY) + + let headers = {} + if (token) { + headers = {'Authorization': token} + } + + this.http = axios.create({ + baseURL: API_BASE_URL, + timeout: 15000, // 15 sec, mert szar a mobilnet + headers: headers + }) + } + + constructor() { + this._setupHTTPObject() + } + + get haveToken() { + return !!localStorage.getItem(LOCAL_STORAGE_KEY) + } + + _performApiCall(method, url, data, precheckToken, expectedStatus, errorTexts = COMMON_ERROR_CODES) { + + return new Promise((resolve, reject) => { + + if (precheckToken && !this.haveToken) { + return reject({ + status: null, + text: "Not logged in", + data: null + }); + } + + this.http.request({ + url, method, data, + validateStatus(status) { + return status === expectedStatus; + } + }).then((response) => { + + return resolve(response.data); + + }).catch((error) => { + + if (!error.response) { // Network error (CORS?) + + return reject({ + status: null, + text: "Network error", + data: null + }) + + } else { // Server side error + + return reject({ + status: error.response.status, + text: errorTexts[error.response.status] || "Network or server error", + data: error.response.data + }) + + } + + }); + + }); + + } + + performLogin(name, password) { + + return new Promise((resolve, reject) => { + + this._performApiCall('post', '/auth/login', {name, password}, false, 201, { + 401: "Invalid credentials", + ...COMMON_ERROR_CODES + }).then((data) => { + + localStorage.setItem(LOCAL_STORAGE_KEY, data.token) + this._setupHTTPObject() // Update JWT token memes + return resolve({name}) + + }).catch(reject) + + }); + + } + + performLogout() { + return new Promise((resolve, reject) => { + + this._performApiCall('delete', '/auth/login', null, true, 204).then(() => { + + localStorage.removeItem(LOCAL_STORAGE_KEY) + this._setupHTTPObject() // Update JWT token memes + return resolve(null) + + }).catch(reject) + + }) + } + + getMyInfo() { + return this._performApiCall('get', '/auth/me', null, true, 200); + } + + getAllLists() { + return this._performApiCall('get', '/lists', null, true, 200); + } + + getList(id) { + return this._performApiCall('get', `/lists/${id}`, null, true, 200); + } + + getTrackFromList(listid, trackid) { + return this._performApiCall('get', `/lists/${listid}/${trackid}`, null, true, 200); + } + +} \ No newline at end of file diff --git a/src/components/FooterNav.vue b/src/components/FooterNav.vue new file mode 100644 index 0000000..5e6e859 --- /dev/null +++ b/src/components/FooterNav.vue @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index 1c544cb..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - diff --git a/src/components/LoadingScreen.vue b/src/components/LoadingScreen.vue index 2ec58c3..7c80ec2 100644 --- a/src/components/LoadingScreen.vue +++ b/src/components/LoadingScreen.vue @@ -1,13 +1,16 @@ - - \ No newline at end of file diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue new file mode 100644 index 0000000..00d8b8e --- /dev/null +++ b/src/components/Navbar.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index f253456..6af8dfc 100644 --- a/src/main.js +++ b/src/main.js @@ -1,9 +1,19 @@ import Vue from 'vue' -import App from './App.vue' -import router from './router' -import store from './store' +import App from '@/App.vue' +import router from '@/router' +import store from '@/store' +import api from "@/api"; + +import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' + +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-vue/dist/bootstrap-vue.css' Vue.config.productionTip = false +Vue.prototype.$api = api + +Vue.use(BootstrapVue) +Vue.use(IconsPlugin) new Vue({ router, diff --git a/src/router/index.js b/src/router/index.js index d36779e..4dc620c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,6 +1,10 @@ import Vue from 'vue' import VueRouter from 'vue-router' -import Home from '../views/Home.vue' +import Home from '@/views/Home.vue' +import Login from "@/views/Login"; + + +import store from '@/store' Vue.use(VueRouter) @@ -8,7 +12,12 @@ const routes = [ { path: '/', name: 'Home', - component: Home + component: Home, + meta: { + allowVisit(authorized) { + return authorized; + } + } }, { path: '/about', @@ -16,7 +25,22 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') + component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), + meta: { + allowVisit() { + return true; + } + } + }, + { + path: '/login', + name: 'Login', + component: Login, + meta: { + allowVisit(authorized) { + return !authorized; + } + } } ] @@ -26,4 +50,24 @@ const router = new VueRouter({ routes }) +router.beforeEach((to, from, next) => { + + const authorized = store.getters.isLoggedIn; + + const visitAllowed = to.matched.some(record => record.meta.allowVisit(authorized)) + + if (visitAllowed) { + next(); + return; + } + + if (authorized) { + next({name: 'Home'}) + } else { + next({name: 'Login'}) + } + +}) + + export default router diff --git a/src/store/index.js b/src/store/index.js index 332b916..8d7d234 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -4,12 +4,33 @@ import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ - state: { - }, - mutations: { - }, - actions: { - }, - modules: { - } + state: { + userdata: { + name: null + }, + appReady: false + }, + mutations: { + storeUserData(state, username) { + state.userdata.name = username; + }, + setAppReady(state) { + state.appReady = true; + } + + }, + actions: { + storeUserData({commit}, username) { + commit('storeUserData', username); + }, + setAppReady({commit}) { + commit('setAppReady'); + } + }, + modules: {}, + getters: { + isLoggedIn(state) { + return !!state.userdata.name; + } + } }) diff --git a/src/views/About.vue b/src/views/About.vue index 3fa2807..05ed36f 100644 --- a/src/views/About.vue +++ b/src/views/About.vue @@ -1,5 +1,7 @@ diff --git a/src/views/Home.vue b/src/views/Home.vue index 8bd6c57..bc50fab 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,18 +1,17 @@ diff --git a/src/views/Login.vue b/src/views/Login.vue new file mode 100644 index 0000000..814034c --- /dev/null +++ b/src/views/Login.vue @@ -0,0 +1,75 @@ + + + + + \ No newline at end of file diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000..d64a222 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,30 @@ +module.exports = { + publicPath: "/", + chainWebpack: config => { + config.module + .rule('vue') + .use('vue-loader') + .loader('vue-loader') + .tap(options => { + options.transformAssetUrls = { + img: 'src', + image: 'xlink:href', + 'b-avatar': 'src', + 'b-img': 'src', + 'b-img-lazy': ['src', 'blank-src'], + 'b-card': 'img-src', + 'b-card-img': 'src', + 'b-card-img-lazy': ['src', 'blank-src'], + 'b-carousel-slide': 'img-src', + 'b-embed': 'src' + } + + return options + }), + config.plugin('html') + .tap(args => { + args[0].title = "onSpot" + return args + }) + } +} \ No newline at end of file