From 21f8554cda92f9c01e65cc40430aec302070c9b3 Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Wed, 25 Oct 2023 17:58:26 +0200 Subject: [PATCH] feat: implemented login page --- package-lock.json | 175 +++++++++++++++++++++++++++++++++ package.json | 7 ++ src/components/LandingPage.vue | 2 +- src/composables/useAuth.ts | 53 ++++++++++ src/i18n/en-US.json | 12 ++- src/main.ts | 53 +++++++++- src/views/HomeView.vue | 4 +- src/views/LoginView.vue | 80 ++++++++++++--- 8 files changed, 367 insertions(+), 19 deletions(-) create mode 100644 src/composables/useAuth.ts diff --git a/package-lock.json b/package-lock.json index b12959b..f1ba977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,12 @@ "@commitlint/cli": "^18.0.0", "@commitlint/config-conventional": "^18.0.0", "@fontsource/poppins": "^5.0.8", + "@fortawesome/free-regular-svg-icons": "github:nethesis/Font-Awesome#regular", + "@fortawesome/free-solid-svg-icons": "github:nethesis/Font-Awesome#solid", + "@fortawesome/vue-fontawesome": "^3.0.3", + "@headlessui/vue": "^1.7.16", + "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", + "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", "@nethserver/vue-tailwind-lib": "^0.0.86", "@rushstack/eslint-patch": "^1.3.3", "@tailwindcss/forms": "^0.5.6", @@ -22,6 +28,7 @@ "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.4.0", "autoprefixer": "^10.4.16", + "axios": "^1.5.1", "eslint": "^8.49.0", "eslint-plugin-vue": "^9.17.0", "husky": "^8.0.0", @@ -1720,6 +1727,29 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.4.0", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#5fea73bd42712ab923637e599ce45fefa6c7bd91", + "dev": true, + "hasInstallScript": true, + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz", + "integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.4.0", "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#b4030d0e6db57a6eb408f6da6b9ddbdc22793c93", @@ -1899,6 +1929,52 @@ "node": ">=8" } }, + "node_modules/@nethesis/nethesis-light-svg-icons": { + "version": "6.2.1", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#3e110fb0ae63495fb8b7ca9811e5f07eed1ac962", + "dev": true, + "hasInstallScript": true, + "license": "UNLICENSED", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.2.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nethesis/nethesis-light-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz", + "integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nethesis/nethesis-solid-svg-icons": { + "version": "6.2.1", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#3e8568f985899ccbb0a0010b14389149ed7a71cd", + "dev": true, + "hasInstallScript": true, + "license": "UNLICENSED", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.2.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nethesis/nethesis-solid-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz", + "integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@nethserver/vue-tailwind-lib": { "version": "0.0.86", "resolved": "https://registry.npmjs.org/@nethserver/vue-tailwind-lib/-/vue-tailwind-lib-0.0.86.tgz", @@ -2870,6 +2946,12 @@ "node": ">=0.10.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -2907,6 +2989,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3182,6 +3275,18 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3507,6 +3612,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4135,6 +4249,40 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -5108,6 +5256,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -5932,6 +6101,12 @@ } } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/package.json b/package.json index 56f5d56..80d64d2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,12 @@ "@commitlint/cli": "^18.0.0", "@commitlint/config-conventional": "^18.0.0", "@fontsource/poppins": "^5.0.8", + "@fortawesome/free-regular-svg-icons": "github:nethesis/Font-Awesome#regular", + "@fortawesome/free-solid-svg-icons": "github:nethesis/Font-Awesome#solid", + "@fortawesome/vue-fontawesome": "^3.0.3", + "@headlessui/vue": "^1.7.16", + "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", + "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", "@nethserver/vue-tailwind-lib": "^0.0.86", "@rushstack/eslint-patch": "^1.3.3", "@tailwindcss/forms": "^0.5.6", @@ -27,6 +33,7 @@ "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.4.0", "autoprefixer": "^10.4.16", + "axios": "^1.5.1", "eslint": "^8.49.0", "eslint-plugin-vue": "^9.17.0", "husky": "^8.0.0", diff --git a/src/components/LandingPage.vue b/src/components/LandingPage.vue index d77632a..6425852 100644 --- a/src/components/LandingPage.vue +++ b/src/components/LandingPage.vue @@ -1,3 +1,3 @@ diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts new file mode 100644 index 0000000..a97d5d5 --- /dev/null +++ b/src/composables/useAuth.ts @@ -0,0 +1,53 @@ +import { computed } from 'vue' +import axios from 'axios' + +interface LoginResponse { + token: string + expire: string +} + +const TOKEN_KEY = 'token' +const EXPIRE_KEY = 'expire' + +export function useAuth() { + /** + * Token from localStorage + */ + const token = computed((): string | null => localStorage.getItem(TOKEN_KEY)) + + /** + * Expire date from localStorage + */ + const expire = computed((): string | null => localStorage.getItem(EXPIRE_KEY)) + + /** + * Whether the user has previously logged in or not + */ + const previouslyLogged = computed((): boolean => { + return token.value != null && expire.value != null + }) + + /** + * Login to the API, saving the token and expire date to localStorage + * @param username + * @param password + */ + async function login(username: string, password: string) { + const response = await axios.post('/api/login', { + username: username, + password: password + }) + localStorage.setItem(TOKEN_KEY, response.data.token) + localStorage.setItem(EXPIRE_KEY, response.data.expire) + } + + /** + * Removes authorization data from localStorage. + */ + function logout() { + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(EXPIRE_KEY) + } + + return { token, expire, previouslyLogged, login, logout } +} diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 1d8c11f..81b8c7e 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -1,10 +1,18 @@ { + "errors": { + "connection_aborted": "Couldn't connect to the server", + "network": "Network error" + }, + "base_template": { + "logout": "Logout" + }, "login_form": { "welcome": "Welcome", "sign_in_description": "Sign in to access your account settings", "username": "Username", "password": "Password", "remember_me": "Remember me", - "sign_in": "Sign in" + "sign_in": "Sign in", + "invalid_credentials": "Invalid credentials" } -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index c1d1027..2367bbc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import router from './router' import { createI18n } from 'vue-i18n' import enJson from './i18n/en-US.json' import itJson from './i18n/it-IT.json' +import axios, { AxiosError } from 'axios' const app = createApp(App) @@ -19,12 +20,58 @@ app.use(router) app.use( createI18n({ locale: navigator.language, - fallbackLocale: 'en', + fallbackLocale: 'en-US', messages: { - en: enJson, - it: itJson + 'en-US': enJson, + 'it-IT': itJson } }) ) +// setup axios +axios.defaults.baseURL = import.meta.env.VITE_ENDPOINT ?? '' +axios.defaults.timeout = 500 + +axios.interceptors.request.use((config) => { + if (localStorage.getItem('token') != null) { + config.headers.Authorization = `Bearer ${localStorage.getItem('token')}` + } + return config +}) + +axios.interceptors.response.use( + (response) => response, + (error) => { + if (axios.isAxiosError(error)) { + const exception = error as AxiosError + switch (exception.code) { + case AxiosError.ECONNABORTED: + exception.message = 'errors.connection_aborted' + break + case AxiosError.ERR_NETWORK: + exception.message = 'errors.network' + break + } + return Promise.reject(exception) + } + return Promise.reject(error) + } +) + +axios.interceptors.response.use( + (response) => response, + (error) => { + if (axios.isAxiosError(error)) { + const exception = error as AxiosError + if (exception.code == AxiosError.ERR_BAD_REQUEST && exception.response?.status == 401) { + localStorage.removeItem('token') + localStorage.removeItem('expire') + router.replace('/login') + } + return Promise.reject(exception) + } + return Promise.reject(error) + } +) + app.mount('#app') diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index c1209cc..dfd44f0 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -3,7 +3,7 @@ import LandingPage from '../components/LandingPage.vue' diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 2c15052..b54fbbc 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -1,26 +1,84 @@