From d76535c75a50c59b6566f3a59b428d92a920eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Roso=C5=82owski?= <87621210+rr-adam@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:04:48 +0200 Subject: [PATCH 1/9] Initial implementation --- general/README.md | 2 + general/api/ontology.js | 166 ++++++++++----- general/api/strapi.js | 1 + general/assets/scss/global.scss | 150 +++++++++++++ .../Articles/SerializationListSection.vue | 12 +- general/components/Header/AuthComponent.vue | 131 ++++++++++++ general/components/Header/AuthLoginModal.vue | 149 +++++++++++++ .../components/Header/AuthRecoverModal.vue | 199 ++++++++++++++++++ .../components/Header/AuthRegisterModal.vue | 184 ++++++++++++++++ general/components/Header/HeaderComponent.vue | 6 +- .../Header/MinimalHeaderComponent.vue | 4 +- .../components/Ontology/DescribeButton.vue | 23 +- .../components/Ontology/UnauthorizedError.vue | 22 ++ general/components/StatsComponent.vue | 8 +- general/components/UI/CustomInput.vue | 77 +++++++ general/components/UI/bs-modal.vue | 13 +- general/helpers/ontograph.js | 31 +-- general/nuxt.config.ts | 7 +- general/package-lock.json | 36 +++- general/package.json | 5 +- general/pages/auth/login.vue | 183 ++++++++++++++++ general/pages/auth/recover-password.vue | 168 +++++++++++++++ general/pages/ontology/[...resource].vue | 69 +++--- general/stores/auth.ts | 48 +++++ general/stores/authModal.ts | 24 +++ general/stores/configuration.ts | 4 +- general/stores/ontology.ts | 22 +- 27 files changed, 1599 insertions(+), 145 deletions(-) create mode 100644 general/components/Header/AuthComponent.vue create mode 100644 general/components/Header/AuthLoginModal.vue create mode 100644 general/components/Header/AuthRecoverModal.vue create mode 100644 general/components/Header/AuthRegisterModal.vue create mode 100644 general/components/Ontology/UnauthorizedError.vue create mode 100644 general/components/UI/CustomInput.vue create mode 100644 general/pages/auth/login.vue create mode 100644 general/pages/auth/recover-password.vue create mode 100644 general/stores/auth.ts create mode 100644 general/stores/authModal.ts diff --git a/general/README.md b/general/README.md index 337d46fd..71ef907d 100644 --- a/general/README.md +++ b/general/README.md @@ -66,6 +66,8 @@ The following values can be defined in `variables`: - `jenkinsJobUrl` - used to group versions based on Jenkins jobs. DEFAULT: `null`, EXAMPLE: `https://jenkins.edmcouncil.org/job/idmp/` +- `authEnabled` - used to enable authorization feature, login and register functionality will be enabled when value is set to `true`. + ## Build and run frontend - create directory for frontend application instance diff --git a/general/api/ontology.js b/general/api/ontology.js index ff07c156..235c9348 100644 --- a/general/api/ontology.js +++ b/general/api/ontology.js @@ -1,3 +1,47 @@ +import axios from 'axios'; +import { + useAuthStore, + useOntologyStore, + useConfigurationStore +} from '#imports'; +export const axiosClient = axios.create(); + +axiosClient.interceptors.request.use((config) => { + const authStore = useAuthStore(); + const configStore = useConfigurationStore(); + + const currentOrigin = window.location.origin; + const requestUrl = new URL(config.url, currentOrigin); + + if ( + configStore.config.authEnabled === 'true' && + authStore.jwt && + !config.noAuth && + requestUrl.origin === currentOrigin + ) { + config.headers.Authorization = `Bearer ${authStore.jwt}`; + } + + return config; +}); + +axiosClient.interceptors.response.use( + (response) => response, + (error) => { + const authStore = useAuthStore(); + const ontologyStore = useOntologyStore(); + + if (error.response && error.response.status === 401) { + authStore.clear(); + } + if (error.response && error.response.status === 403) { + ontologyStore.unauthorizedError = true; + } + + return Promise.reject(error); + } +); + function ServerError(message, status) { this.message = message; this.status = status; @@ -10,59 +54,83 @@ const parseServerError = (response) => { return response; }; -const getEntity = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); +const getEntity = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); -const getModules = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); +const getModules = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); -const getOntologyVersions = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); +const getOntologyVersions = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); const getJenkinsJobs = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); - -const getFindSearch = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); - -const getFindProperties = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); - -const getStats = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); - -const getMissingImports = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/json' } - }).then(parseServerError); - -const getDescribeIntegration = (domain) => - fetch(domain, { - method: 'GET', - headers: { Accept: 'application/rdf+xml' } - }).then(parseServerError); + axiosClient + .get(domain, { noAuth: true, headers: { Accept: 'application/json' } }) + .then(parseServerError) + .then((response) => response.data); + +const getFindSearch = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); + +const getFindProperties = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); + +const getStats = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); + +const getMissingImports = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/json', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); + +const getDescribeIntegration = (domain, config = {}) => + axiosClient + .get(domain, { + ...config, + headers: { Accept: 'application/rdf+xml', ...config.headers } + }) + .then(parseServerError) + .then((response) => response.data); export { getEntity, diff --git a/general/api/strapi.js b/general/api/strapi.js index ad05e8d8..fda637de 100644 --- a/general/api/strapi.js +++ b/general/api/strapi.js @@ -174,6 +174,7 @@ export async function getAppConfigurationData(runtimeConfig) { data.uriSpace = response.data.attributes.uriSpace; data.ontologyRepositoryUrl = response.data.attributes.ontologyRepositoryUrl; data.jenkinsJobUrl = response.data.attributes.jenkinsJobUrl; + data.authEnabled = response.data.attributes.authEnabled; // overwrite with 'variables' data const { variables } = response.data.attributes; diff --git a/general/assets/scss/global.scss b/general/assets/scss/global.scss index 2fc02210..f4591412 100644 --- a/general/assets/scss/global.scss +++ b/general/assets/scss/global.scss @@ -236,6 +236,7 @@ body.modal-open { } &:focus { background: $normal-button-focus-bg; + color: rgba(255, 255, 255, 0.9); box-shadow: rgba(0, 0, 0, 0.4) 0px 0px 0px 4px; } &:active { @@ -651,3 +652,152 @@ a.deprecated { a.deprecated:hover { color: #ec241d !important; } + +.modal-card { + .modal-error { + color: #ec241d; + } + + .modal-form { + display: flex; + flex-direction: column; + + .muted-link { + cursor: pointer; + align-self: flex-end; + color: rgba(0, 0, 0, 0.6); + + &:focus-visible { + box-shadow: $form-focus-shadow; + } + + &:active { + filter: none; + box-shadow: none; + } + } + + .normal-button { + width: fit-content; + } + + .alert { + color: rgba(0, 0, 0, 0.8); + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.01em; + border: none; + border-radius: 2px; + margin-top: 20px; + padding: 5px 15px 5px 15px; + background-color: #ec241d; + } + } +} + +.modal.login-modal, +.modal.recover-modal, +.modal.register-modal { + .modal-header { + position: relative; + box-shadow: 0px 5px 20px -5px rgba(8, 84, 150, 0.15); + border: none; + padding: 15px 40px; + + justify-content: space-between; + + .close-btn { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + width: 24px; + height: 30px; + padding: 0; + margin-left: 20px; + + &::before { + content: ''; + background-image: url('../../assets/icons/close.svg'); + background-repeat: no-repeat; + background-size: 24px 24px; + width: 24px; + height: 24px; + } + } + + .return-btn { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + width: 24px; + height: 30px; + padding: 0; + margin-right: 20px; + + &::before { + content: ''; + background-image: url('../../assets/icons/return-arrow.svg'); + background-repeat: no-repeat; + background-size: 24px 24px; + width: 24px; + height: 24px; + } + } + + .left { + display: flex; + align-items: center; + } + + h5.modal-title { + font-style: normal; + font-weight: bold; + font-size: 18px; + line-height: 30px; + color: #000000; + + padding: 0; + margin: 0; + position: relative; + } + } + .modal-content { + border-radius: 2px; + border: none; + background: white; + height: 100%; + } + .modal-body { + padding: 30px 40px; + height: 100%; + position: relative; + + a { + color: rgba(0, 0, 0, 0.8); + text-decoration: none; + + &:hover { + color: $link-hover-color; + } + + &:active { + color: $link-active-color; + } + } + } + .modal-footer { + border: none; + box-shadow: 0px -5px 20px -5px rgba(8, 84, 150, 0.15); + padding: 18px 30px; + + justify-content: space-between; + + .normal-button.small { + margin-bottom: 0 !important; + } + } +} diff --git a/general/components/Articles/SerializationListSection.vue b/general/components/Articles/SerializationListSection.vue index cd7507cd..8f109d1e 100644 --- a/general/components/Articles/SerializationListSection.vue +++ b/general/components/Articles/SerializationListSection.vue @@ -306,10 +306,9 @@ export default { }, async fetchVersions() { try { - const result = await getOntologyVersions( + const ontologyVersions = await getOntologyVersions( `/${this.ontologyName}/ontology/api/` ); - const ontologyVersions = await result.json(); const first = 'master/latest'; ontologyVersions.sort((x, y) => @@ -352,24 +351,21 @@ export default { const tagName = runtimeConfig.public.tagName; // group versions by tags, pull requests and releases - const tagsResult = await getJenkinsJobs( + const tagsJson = await getJenkinsJobs( `${jenkinsJobUrl}/view/tags/api/json` ); - const tagsJson = await tagsResult.json(); const tags = tagsJson.jobs.map((item) => item.name.toLowerCase()); - const pullRequestsResult = await getJenkinsJobs( + const pullRequestsJson = await getJenkinsJobs( `${jenkinsJobUrl}/view/change-requests/api/json` ); - const pullRequestsJson = await pullRequestsResult.json(); const pullRequests = pullRequestsJson.jobs.map((item) => item.name.toLowerCase() ); - const defaultViewResult = await getJenkinsJobs( + const defaultViewJson = await getJenkinsJobs( `${jenkinsJobUrl}/view/default/api/json` ); - const defaultViewJson = await defaultViewResult.json(); const defaultView = defaultViewJson.jobs.map((item) => item.name.toLowerCase() ); diff --git a/general/components/Header/AuthComponent.vue b/general/components/Header/AuthComponent.vue new file mode 100644 index 00000000..64e627cc --- /dev/null +++ b/general/components/Header/AuthComponent.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/general/components/Header/AuthLoginModal.vue b/general/components/Header/AuthLoginModal.vue new file mode 100644 index 00000000..382b8ef2 --- /dev/null +++ b/general/components/Header/AuthLoginModal.vue @@ -0,0 +1,149 @@ + + + diff --git a/general/components/Header/AuthRecoverModal.vue b/general/components/Header/AuthRecoverModal.vue new file mode 100644 index 00000000..09d5ea7c --- /dev/null +++ b/general/components/Header/AuthRecoverModal.vue @@ -0,0 +1,199 @@ + + + diff --git a/general/components/Header/AuthRegisterModal.vue b/general/components/Header/AuthRegisterModal.vue new file mode 100644 index 00000000..a69386ee --- /dev/null +++ b/general/components/Header/AuthRegisterModal.vue @@ -0,0 +1,184 @@ + + + diff --git a/general/components/Header/HeaderComponent.vue b/general/components/Header/HeaderComponent.vue index e7f81155..3e283394 100644 --- a/general/components/Header/HeaderComponent.vue +++ b/general/components/Header/HeaderComponent.vue @@ -3,7 +3,7 @@
-