From d0e53fee3b64182b6c87e071bffac3a99c66196e Mon Sep 17 00:00:00 2001 From: Luka Romih Date: Mon, 8 May 2023 23:25:25 +0200 Subject: [PATCH] Fix most glaring bugs and vulnerabilities --- .prettierignore | 1 - concordancer/Dockerfile | 2 +- concordancer/Dockerfile.prod | 2 +- docker-compose.prod.yml | 4 +- docker-compose.yml | 4 +- express/app.js | 13 +- express/bin/www | 7 +- express/config/init.js | 56 ++- express/config/settings.js | 4 + express/controllers/api/v1/comments.js | 76 +++- express/controllers/api/v1/consultancy.js | 73 ++-- express/controllers/api/v1/demo-paginacija.js | 15 - express/controllers/api/v1/dictionaries.js | 49 ++- express/controllers/api/v1/extraction.js | 9 +- express/controllers/api/v1/search.js | 8 +- express/controllers/api/v1/system.js | 7 +- express/controllers/api/v1/users.js | 111 +++++- express/controllers/consultancy.js | 36 +- express/controllers/demo-paginacija.js | 17 - express/controllers/dictionaries.js | 128 ++++--- express/controllers/extraction-poc.js | 150 -------- express/controllers/extraction.js | 31 +- express/controllers/helpers/dictionary.js | 32 +- .../search-filter-data-suggestion-importer.js | 21 +- express/controllers/index.js | 73 +++- express/controllers/portals.js | 12 +- express/controllers/users.js | 117 +++++- express/middleware/auth.js | 9 +- express/middleware/index.js | 7 + express/middleware/user.js | 88 ++++- express/models/comment.js | 260 +------------ express/models/consultancy-entry.js | 51 --- express/models/demo-paginacija.js | 49 --- express/models/dictionary.js | 124 +++--- express/models/entry.js | 140 +++---- express/models/extraction.js | 153 ++++++-- .../models/helpers/dictionary/import-file.js | 76 +--- express/models/helpers/dictionary/index.js | 100 ++++- express/models/helpers/extraction.js | 1 + express/models/helpers/search/index.js | 5 +- express/models/helpers/user/index.js | 1 + express/models/portal.js | 9 +- express/models/user.js | 352 ++++++++++++++++-- express/package-lock.json | 14 + express/package.json | 1 + express/public/images/alert-circle-big.svg | 5 + express/public/javascripts/admin.js | 209 +++++++---- express/public/javascripts/demo-paginacija.js | 188 ---------- express/public/javascripts/dictionaries.js | 33 +- .../dictionary-import-extraction.js | 4 + .../javascripts/extraction-files-edit.js | 2 +- .../javascripts/extraction-poc-files-edit.js | 128 ------- .../public/javascripts/extraction-poc-list.js | 22 -- .../javascripts/extraction-poc-oss-edit.js | 95 ----- .../extraction-poc-term-candidates.js | 144 ------- express/public/javascripts/profile-scripts.js | 27 +- express/public/javascripts/reset-password.js | 71 ++-- express/public/javascripts/scripts.js | 138 +++---- express/public/javascripts/search-results.js | 30 +- express/public/locales/en/core.json | 40 +- express/public/locales/sl/core.json | 34 +- express/routes/admin.js | 18 +- express/routes/api/v1/comments.js | 8 - express/routes/api/v1/consultancy.js | 6 - express/routes/api/v1/demo-paginacija.js | 7 - express/routes/api/v1/dictionaries.js | 63 +++- express/routes/api/v1/entries.js | 4 +- express/routes/api/v1/extraction.js | 62 ++- express/routes/api/v1/index.js | 2 - express/routes/api/v1/portals.js | 4 + express/routes/api/v1/search.js | 4 +- express/routes/api/v1/system.js | 8 +- express/routes/api/v1/users.js | 33 +- express/routes/consultancy.js | 47 ++- express/routes/dictionaries.js | 9 +- express/routes/extraction-poc.js | 27 -- express/routes/extraction.js | 50 +-- express/routes/index.js | 75 +--- express/utils/index.js | 2 + .../common/side-menu-mixin-consultancy.pug | 2 +- .../components/login-signup/login-signup.pug | 2 +- .../main-navigation-right-mixin.pug | 18 +- .../advanced-search-modal-area-only.pug | 2 +- .../advanced-search-modal.pug | 10 +- .../search-and-filter/result-item.pug | 4 +- .../search-and-filter/search-filter-modal.pug | 8 +- .../views/email/dictionary-status-change.pug | 10 +- .../email/user-change-email-success_en.pug | 2 + .../email/user-change-email-success_sl.pug | 2 + .../email/user-change-email-token_en.pug | 4 + .../email/user-change-email-token_sl.pug | 4 + .../views/email/user-change-password_en.pug | 2 + .../views/email/user-change-password_sl.pug | 2 + .../email/user-reset-password-success_en.pug | 2 + .../email/user-reset-password-success_sl.pug | 2 + .../email/user-reset-password-token_en.pug | 4 + .../email/user-reset-password-token_sl.pug | 4 + express/views/extraction-poc/docs-edit.pug | 29 -- express/views/extraction-poc/edit-oss.pug | 128 ------- express/views/extraction-poc/edit-own.pug | 20 - express/views/extraction-poc/list.pug | 44 --- .../views/extraction-poc/stop-terms-edit.pug | 22 -- .../views/extraction-poc/term-candidates.pug | 23 -- express/views/layout.pug | 7 + express/views/pages/admin/areas.pug | 6 +- .../views/pages/admin/connections-list.pug | 4 +- .../pages/admin/dictionary-description.pug | 3 +- .../pages/admin/dictionary-structure.pug | 3 +- .../views/pages/admin/dictionary-users.pug | 2 +- express/views/pages/admin/user-list.pug | 2 +- .../views/pages/consultancy/admin/edit.pug | 2 +- express/views/pages/consultancy/ask.pug | 7 +- express/views/pages/demo-paginacija.pug | 50 --- .../views/pages/dictionaries/description.pug | 5 +- express/views/pages/dictionaries/dictlist.pug | 2 +- express/views/pages/dictionaries/list.pug | 2 +- express/views/pages/dictionaries/new.pug | 2 +- .../views/pages/dictionaries/structure.pug | 2 +- express/views/pages/dictionaries/users.pug | 3 +- express/views/pages/extraction/edit-oss.pug | 248 +----------- express/views/pages/help-pug-demo-frame.pug | 8 - express/views/pages/help-pug-demo.pug | 190 ---------- .../views/pages/profile/change-password.pug | 8 +- .../views/pages/profile/delete-profile.pug | 2 + .../profile/my-profile-side-menu-mixin.pug | 18 +- express/views/pages/profile/my-profile.pug | 16 +- .../pages/reset-password/reset-password.pug | 9 +- ...ultancy-admin-in-progress-panel-header.pug | 2 +- express/views/utilities/content-edit.pug | 4 +- .../dictionary-description-mixin.pug | 6 +- .../dictionary-extraction-import-mixin.pug | 2 +- .../views/utilities/dictionary-main-input.pug | 4 +- .../utilities/dictionary-structure-input.pug | 9 +- .../views/utilities/entry-preview-mixin.pug | 2 +- express/views/utilities/entry-preview.pug | 4 +- express/views/utilities/modal-info.pug | 27 ++ .../utilities/modal-reset-password-info.pug | 10 +- .../modal-reset-password-success.pug | 2 +- .../views/utilities/modal-reset-password.pug | 2 +- .../utilities/result-detail-side-menu.pug | 4 +- postgres/init/006_auth_and_aut.sql | 16 +- postgres/init/018_extraction.sql | 2 +- .../019_extraction_oss_document_types.sql | 68 ++++ ...y_export.sql => 020_dictionary_export.sql} | 0 144 files changed, 2231 insertions(+), 2981 deletions(-) delete mode 100644 express/controllers/api/v1/demo-paginacija.js delete mode 100644 express/controllers/demo-paginacija.js delete mode 100644 express/controllers/extraction-poc.js delete mode 100644 express/models/demo-paginacija.js create mode 100644 express/public/images/alert-circle-big.svg delete mode 100644 express/public/javascripts/demo-paginacija.js delete mode 100644 express/public/javascripts/extraction-poc-files-edit.js delete mode 100644 express/public/javascripts/extraction-poc-list.js delete mode 100644 express/public/javascripts/extraction-poc-oss-edit.js delete mode 100644 express/public/javascripts/extraction-poc-term-candidates.js delete mode 100644 express/routes/api/v1/demo-paginacija.js delete mode 100644 express/routes/extraction-poc.js create mode 100644 express/views/email/user-change-email-success_en.pug create mode 100644 express/views/email/user-change-email-success_sl.pug create mode 100644 express/views/email/user-change-email-token_en.pug create mode 100644 express/views/email/user-change-email-token_sl.pug create mode 100644 express/views/email/user-change-password_en.pug create mode 100644 express/views/email/user-change-password_sl.pug create mode 100644 express/views/email/user-reset-password-success_en.pug create mode 100644 express/views/email/user-reset-password-success_sl.pug create mode 100644 express/views/email/user-reset-password-token_en.pug create mode 100644 express/views/email/user-reset-password-token_sl.pug delete mode 100644 express/views/extraction-poc/docs-edit.pug delete mode 100644 express/views/extraction-poc/edit-oss.pug delete mode 100644 express/views/extraction-poc/edit-own.pug delete mode 100644 express/views/extraction-poc/list.pug delete mode 100644 express/views/extraction-poc/stop-terms-edit.pug delete mode 100644 express/views/extraction-poc/term-candidates.pug delete mode 100644 express/views/pages/demo-paginacija.pug delete mode 100644 express/views/pages/help-pug-demo-frame.pug delete mode 100644 express/views/pages/help-pug-demo.pug create mode 100644 express/views/utilities/modal-info.pug create mode 100644 postgres/init/019_extraction_oss_document_types.sql rename postgres/init/{019_dictionary_export.sql => 020_dictionary_export.sql} (100%) diff --git a/.prettierignore b/.prettierignore index 0011463..fdd16db 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ old_express express/public/stylesheets -help-pug-demo.pug diff --git a/concordancer/Dockerfile b/concordancer/Dockerfile index 67ab6ad..69850f3 100644 --- a/concordancer/Dockerfile +++ b/concordancer/Dockerfile @@ -2,5 +2,5 @@ FROM alpine:3.16 AS builder WORKDIR /sloleks RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip - -FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.3 +FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4 COPY --from=builder /sloleks /sloleks diff --git a/concordancer/Dockerfile.prod b/concordancer/Dockerfile.prod index c565830..bf3ac67 100644 --- a/concordancer/Dockerfile.prod +++ b/concordancer/Dockerfile.prod @@ -2,5 +2,5 @@ FROM alpine:3.16 AS builder WORKDIR /sloleks RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip - -FROM ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.3 +FROM ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4 COPY --from=builder /sloleks /sloleks diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1f8265e..b6b9d5a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -95,7 +95,7 @@ services: - "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601" concordancer: - # image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.3 + # image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4 build: context: concordancer dockerfile: Dockerfile.prod @@ -115,7 +115,7 @@ services: # Disabled by default. Enable if needed. concordancer-manager: - image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.3 + image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4 profiles: - concordancer-manager depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 4623294..ea6aec0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -102,7 +102,7 @@ services: - "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601" concordancer: - # image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.3 + # image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4 build: concordancer profiles: - develop-concordancer @@ -121,7 +121,7 @@ services: - sloleks:/sloleks concordancer-manager: - image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.3 + image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4 profiles: - develop-concordancer depends_on: diff --git a/express/app.js b/express/app.js index 3f7912c..5be8a26 100644 --- a/express/app.js +++ b/express/app.js @@ -6,6 +6,7 @@ const helmet = require('helmet') const favicon = require('serve-favicon') const cookieParser = require('cookie-parser') const createError = require('http-errors') +const flash = require('connect-flash-plus') const i18next = require('i18next') const i18nextMiddleware = require('i18next-http-middleware') // const debug = require('debug')('termPortal:app') @@ -18,7 +19,8 @@ const i18n = require('./middleware/i18n') const passport = require('./middleware/auth') const user = require('./middleware/user') const settings = require('./middleware/settings') -const { enhanceLocals } = require('./middleware') +const { enhanceLocals, adjustHeaders } = require('./middleware') +const { capitalize } = require('./utils') // Import Routers. const apiRouter = require('./routes/api') @@ -41,16 +43,22 @@ app.locals.basedir = viewsPath const inDevEnv = app.get('env') === 'development' if (isBehindProxy) app.set('trust proxy', 1) // Trust first proxy. app.locals.inDevEnv = inDevEnv +app.locals.capitalize = capitalize // Mount middleware. app.use(logger('dev')) app.use(helmet(helmetConfig)) app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico'))) -app.use(express.static(path.join(__dirname, 'public'))) +app.use( + express.static(path.join(__dirname, 'public'), { + maxAge: inDevEnv ? 0 : '1h' + }) +) app.use(express.json({ type: ['application/json', 'application/csp-report'] })) app.use(express.urlencoded({ extended: true })) app.use(cookieParser(secret)) app.use(session) +app.use(flash()) app.use(passport.initialize()) app.use(passport.session()) app.use(passport.authenticate('remember-me')) @@ -59,6 +67,7 @@ app.use(i18nextMiddleware.handle(i18next)) app.use(user.enhance) app.use(settings.prepareRequiredSettings) app.use(enhanceLocals) +app.use(adjustHeaders) if (inDevEnv) { app.post( diff --git a/express/bin/www b/express/bin/www index dfac841..03368ff 100755 --- a/express/bin/www +++ b/express/bin/www @@ -12,8 +12,6 @@ const cache = require('../models/cache') const searchEngine = require('../models/search-engine') const email = require('../models/email') const init = require('../config/init') -const { seedDummyData } = require('../models/comment') -// const { initDemoData } = require('../models/demo-paginacija') const app = require('../app') const debug = require('debug')('termPortal:server') const http = require('http') @@ -42,12 +40,11 @@ const server = http.createServer(app) cache.waitForConnection(), searchEngine.waitForConnection(), email.waitForConnection(), - init() + init.fsStructure() ]) await searchEngine.initEntryIndex() await searchEngine.initConsultancyEntryIndex() - seedDummyData() - // initDemoData() + await init.adminUser() server.listen(port) })() diff --git a/express/config/init.js b/express/config/init.js index b88529f..0d7ca46 100644 --- a/express/config/init.js +++ b/express/config/init.js @@ -1,6 +1,60 @@ const { mkdir } = require('fs/promises') +const db = require('../models/db') +const User = require('../models/user') +const { + portalAdminInitialEmail, + portalAdminInitialPassword +} = require('../config/keys') const { TEMP_EXPORT_PATH } = require('./settings') +const debug = require('debug')('termPortal:config/init') -module.exports = async () => { +exports.fsStructure = async () => { await mkdir(TEMP_EXPORT_PATH, { recursive: true }) } + +async function createPortalAdmin() { + const { + rows: [{ exists }] + } = await db.query( + "SELECT EXISTS (SELECT 1 FROM user_role WHERE role_name = 'portal admin')" + ) + + if (exists) return 'Skipping creation of portal admin (already exists)' + + const ADMIN_BASE = 'admin' + const adminUser = { + username: ADMIN_BASE, + firstName: ADMIN_BASE, + lastName: ADMIN_BASE, + password: portalAdminInitialPassword, + email: portalAdminInitialEmail + } + const userId = await User.create(adminUser) + const assignAdminRole = db.query( + `INSERT INTO user_role (user_id, role_name) + VALUES + ($1, 'portal admin'), + ($1, 'dictionaries admin'), + ($1, 'consultancy admin'), + ($1, 'consultant')`, + [userId] + ) + const activateAdminUser = db.query( + `UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`, + [adminUser.username] + ) + await Promise.all([assignAdminRole, activateAdminUser]) + return `Successfully created portal admin (username: ${adminUser.username}, password: ${adminUser.password})` +} + +exports.adminUser = async () => { + // Generate portal admin user in empty DB. + // TODO Replace with a more robust solution for production. + try { + const message = await createPortalAdmin() + debug(message) + } catch (error) { + debug('Portal admin not seeded.') + debug(error) + } +} diff --git a/express/config/settings.js b/express/config/settings.js index 0703e36..a530187 100644 --- a/express/config/settings.js +++ b/express/config/settings.js @@ -24,3 +24,7 @@ exports.DATA_FILES_PATH = DATA_FILES_PATH exports.TEMP_EXPORT_PATH = `${DATA_FILES_PATH}/export_temp` exports.MAX_EXTRACTIONS_PER_USER = 5 + +exports.ACTIVATION_TOKEN_VALID_DAYS = 7 + +exports.CHANGE_EMAIL_TOKEN_VALID_DAYS = 7 diff --git a/express/controllers/api/v1/comments.js b/express/controllers/api/v1/comments.js index a4383a0..dd80ded 100644 --- a/express/controllers/api/v1/comments.js +++ b/express/controllers/api/v1/comments.js @@ -11,6 +11,25 @@ exports.listComments = async (req, res) => { filters.ctxId = null } + if (filters.ctxType === 'entry_dict_int') { + const { dictionary_id: dictionaryId } = await Entry.fetch(filters.ctxId) + const isEditor = req.user.hasAnyDictionaryRole(dictionaryId) + const isPortalAdmin = req.user.hasRole('portal admin') + const isDictionariesAdmin = req.user.hasRole('dictionaries admin') + if (!(isEditor || isPortalAdmin || isDictionariesAdmin)) { + return res.status(400).end() + } + } else if (filters.ctxType === 'entry_consult_int') { + const isConsultantForEntry = req.user.isEditorOfConsultancyEntry( + filters.ctxId + ) + const isPortalAdmin = req.user.hasRole('portal admin') + const isConsultancyAdmin = req.user.hasRole('consultancy admin') + if (!(isConsultantForEntry || isPortalAdmin || isConsultancyAdmin)) { + return res.status(400).end() + } + } + const { pages_total: numberOfAllPages, comments, @@ -38,20 +57,55 @@ exports.createComment = async (req, res) => { res.send({ comments, pagesTotal }) } -exports.seedComments = async (req, res) => { - const { commentCount } = req.params - await Comment.seed(commentCount) - res.send(`${commentCount} new comments generated`) -} - -exports.clearComments = async (req, res) => { - await Comment.clear() - res.send('All comments cleared') -} - exports.updateStatus = async (req, res) => { const commentId = req.body.params.id const commentStatus = req.body.params.status + + const { ctxType, ctxId } = await Comment.fetchContextById(commentId) + + let canUpdateStatus = false + switch (ctxType) { + case 'portal': + if (req.user.hasRole('portal admin')) canUpdateStatus = true + break + + case 'dictionary': + if ( + req.user.hasRole('portal admin') || + req.user.hasRole('dictionaries admin') + ) { + canUpdateStatus = true + } + break + + case 'consultancy': + if ( + req.user.hasRole('portal admin') || + req.user.hasRole('consultancy admin') + ) { + canUpdateStatus = true + } + break + + case 'entry_dict_ext': { + const { dictionary_id: dictionaryId } = await Entry.fetch(ctxId) + + if ( + req.user.hasRole('portal admin') || + req.user.hasRole('dictionaries admin') || + req.user.hasDictionaryRole(dictionaryId, 'administration') + ) { + canUpdateStatus = true + } + break + } + + default: + throw Error('Invalid context type') + } + + if (!canUpdateStatus) return res.status(400).end() + await Comment.updateStatus(commentId, commentStatus) res.send('Visibility changed') } diff --git a/express/controllers/api/v1/consultancy.js b/express/controllers/api/v1/consultancy.js index 5199a8f..b481358 100644 --- a/express/controllers/api/v1/consultancy.js +++ b/express/controllers/api/v1/consultancy.js @@ -1,3 +1,4 @@ +const user = require('../../../middleware/user') const ConsultancyEntry = require('../../../models/consultancy-entry') const Domain = require('../../../models/domain') const User = require('../../../models/user') @@ -16,20 +17,6 @@ const generateQuery = require('../../../models/helpers/search/generate-query') const consultancy = {} -consultancy.listEntries = async (req, res) => { - const consultancyEntryList = await ConsultancyEntry.fetchAll() - const data = {} - data.consEntryList = consultancyEntryList - res.send(data) -} - -consultancy.listNewEntries = async (req, res) => { - const consultancyNewEntryList = await ConsultancyEntry.fetchAllNew() - const data = {} - data.consultancyNewEntryList = consultancyNewEntryList - res.send(data) -} - consultancy.sendPaginationData = async (req, res) => { let requestType = req.query.type const isAdminPage = req.query.isAdmin === 'true' @@ -325,9 +312,12 @@ consultancy.publish = async (req, res) => { const author = await User.fetchUser(entry.authorId) const portalName = await getInstanceSetting(`portal_name_${author.language}`) const renderAsync = promisify(req.app.render.bind(req.app)) - const emailHtml = await renderAsync('email/consultancy-publish-notify', { - portalName - }) + const emailHtml = await renderAsync( + `email/consultancy-publish-notify_${author.language}`, + { + portalName + } + ) await email.send({ to: author.email, subject: i18next.t('Objava terminološkega vprašanja', { @@ -342,29 +332,38 @@ consultancy.publish = async (req, res) => { res.send() } -consultancy.updateQuestion = async (req, res) => { - const { id, questionTitle, domain: domainId, question, answer } = req.body +consultancy.updateQuestion = [ + (req, res, next) => { + const { id } = req.body - if (!id) return res.status(400).send({}) + if (!id) return res.status(400).send({}) - if (questionTitle === '' || question === '' || answer === '') { - return res - .status(422) - .send(req.t('Polja naslov, vprašanje in mnenje so obvezna!')) + req.entryId = id + next() + }, + user.canConsultEntry, + async (req, res) => { + const { id, questionTitle, domain: domainId, question, answer } = req.body + + if (questionTitle === '' || question === '' || answer === '') { + return res + .status(422) + .send(req.t('Polja naslov, vprašanje in mnenje so obvezna!')) + } + + const entry = await ConsultancyEntry.fetchById(id) + entry.domainPrimaryId = domainId > 0 ? domainId : null + entry.question = question + entry.answer = answer // helper.removeHtmlTags(answer).trim() + entry.title = questionTitle + + // TODO Luka: Miha, update only fields that were updated. + await ConsultancyEntry.updateQuestion(entry) + await ConsultancyEntry.indexIntoSearchEngine(id, true) + + res.send({}) } - - const entry = await ConsultancyEntry.fetchById(id) - entry.domainPrimaryId = domainId > 0 ? domainId : null - entry.question = question - entry.answer = answer // helper.removeHtmlTags(answer).trim() - entry.title = questionTitle - - // TODO Luka: Miha, update only fields that were updated. - await ConsultancyEntry.updateQuestion(entry) - await ConsultancyEntry.indexIntoSearchEngine(id, true) - - res.send({}) -} +] consultancy.insertNonModerator = async (req, res) => { const questionId = req.body.question_id diff --git a/express/controllers/api/v1/demo-paginacija.js b/express/controllers/api/v1/demo-paginacija.js deleted file mode 100644 index 0c7d2b4..0000000 --- a/express/controllers/api/v1/demo-paginacija.js +++ /dev/null @@ -1,15 +0,0 @@ -const DemoPaginacija = require('../../../models/demo-paginacija') -const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings') - -exports.list = async (req, res) => { - const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE - - const page = +req.query.p > 0 ? +req.query.p : 1 - - const { pages_total: numberOfAllPages, results } = await DemoPaginacija.fetch( - resultsPerPage, - page - ) - - res.send({ page, numberOfAllPages, results }) -} diff --git a/express/controllers/api/v1/dictionaries.js b/express/controllers/api/v1/dictionaries.js index 4cac5e1..3df6a31 100644 --- a/express/controllers/api/v1/dictionaries.js +++ b/express/controllers/api/v1/dictionaries.js @@ -1,4 +1,5 @@ const { rm } = require('fs/promises') +const user = require('../../../middleware/user') const Dictionary = require('../../../models/dictionary') const Entry = require('../../../models/entry') const Extraction = require('../../../models/extraction') @@ -69,7 +70,11 @@ dictionary.deleteEntry = async (req, res) => { await Promise.all([ Dictionary.updateMetadataAfterModifyingEntries(dictionaryId), deleteEntryFromIndex(entryId, true), - minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app) + minEntriesRequirementCheckAndAct.onDelete( + dictionaryId, + req.app, + req.determinedLanguage + ) ]) res.end() @@ -84,7 +89,11 @@ dictionary.deleteAllEntries = async (req, res) => { await Promise.all([ Dictionary.updateMetadataAfterModifyingEntries(dictionaryId), deleteDictionaryEntriesFromIndex(dictionaryId), - minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app) + minEntriesRequirementCheckAndAct.onDelete( + dictionaryId, + req.app, + req.determinedLanguage + ) ]) res.end() @@ -124,12 +133,19 @@ dictionary.delete = async (req, res) => { res.end() } -dictionary.updateDomainLabels = async (req, res) => { - const { dictionaryId, payload } = req.body.params +dictionary.updateDomainLabels = [ + (req, res, next) => { + req.dictionaryId = req.body.params.dictionaryId + next() + }, + user.canAdministrateDictionary, + async (req, res) => { + const { dictionaryId, payload } = req.body.params - await Dictionary.updateDomainLabel(dictionaryId, payload) - res.end() -} + await Dictionary.updateDomainLabel(dictionaryId, payload) + res.end() + } +] dictionary.renovateSecondaryDomains = async (req, res) => { const data = req.body.params.payload @@ -152,7 +168,11 @@ dictionary.listDictionaries = async (req, res) => { const page = +req.query.p > 0 ? +req.query.p : 1 const { pages_total: numberOfAllPages, results } = - await Dictionary.fetchAllAdminDictionaries(resultsPerPage, page) + await Dictionary.fetchAllAdminDictionaries( + req.determinedLanguage, + resultsPerPage, + page + ) res.send({ page, numberOfAllPages, results }) } @@ -222,15 +242,6 @@ dictionary.listSecondaryDomainData = async (req, res) => { }) } -dictionary.listSecondaryDomains = async (req, res) => { - const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE - const page = +req.query.p > 0 ? +req.query.p : 1 - const { pages_total: numberOfAllPages, results } = - await Dictionary.fetchAllSecondaryDomains(resultsPerPage, page) - - res.send({ page, numberOfAllPages, results }) -} - dictionary.showImportFromFileForm = async (req, res) => { const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE const { dictionaryId } = req.params @@ -252,7 +263,7 @@ dictionary.showExportToFileForm = async (req, res) => { } dictionary.importFromExtraction = async (req, res) => { - const { id: dictionaryId, extractionId } = req.params + const { dictionaryId, extractionId } = req.params const { from, to } = req.body const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0 const toIndex = Number.isInteger(+(to === '' ? undefined : to)) @@ -279,7 +290,7 @@ dictionary.importFromExtraction = async (req, res) => { } dictionary.exportBegin = async (req, res) => { - const dictionaryId = req.params.id + const { dictionaryId } = req.params const exportParams = { isValidFilter: req.body.isValidFilter === 'on' ? undefined : req.body.isValidFilter, diff --git a/express/controllers/api/v1/extraction.js b/express/controllers/api/v1/extraction.js index 59b5212..b705363 100644 --- a/express/controllers/api/v1/extraction.js +++ b/express/controllers/api/v1/extraction.js @@ -166,7 +166,7 @@ extraction.ossSearch = [ extraction.ossConfirmParams = async (req, res) => { const { id: extractionId } = req.params - const { ossParams } = await Extraction.fetch(extractionId) + const { ossParams } = req.extractionData if (ossParams.status !== 'valid') throw Error('OSS params not valid') await Extraction.updateOssParams(extractionId, { params: ossParams.params, @@ -177,7 +177,7 @@ extraction.ossConfirmParams = async (req, res) => { extraction.begin = async (req, res) => { const extractionId = req.params.id - const extraction = await Extraction.fetch(extractionId) + const extraction = req.extractionData const canBegin = await checkIfcanBegin(extraction) if (!canBegin) throw Error('Extraction does not qualify to be ran') @@ -268,11 +268,6 @@ extraction.termCandidatesExport = async (req, res) => { } } -extraction.listFinishedForUser = async (req, res) => { - const extractions = await Extraction.fetchFinishedForUser(req.user.id) - res.send(extractions) -} - extraction.listTermCandidates = async (req, res) => { const extractionId = req.params.id const termCandidatesJson = await Extraction.fetchTermCandidatesJson( diff --git a/express/controllers/api/v1/search.js b/express/controllers/api/v1/search.js index 5ec7784..4d2ea89 100644 --- a/express/controllers/api/v1/search.js +++ b/express/controllers/api/v1/search.js @@ -178,7 +178,8 @@ exports.listFilteredDictionaries = async (req, res) => { hitsPerPage, page, orderAttribute, - orderIndex + orderIndex, + req.determinedLanguage ) dictionaries = dictionaries.map(e => { @@ -246,6 +247,9 @@ exports.showModalFilterResults = async (req, res) => { const aggregationRaw = await searchEntryIndex(aggregateQuery) - const aggregation = await prepareAggregation(aggregationRaw) + const aggregation = await prepareAggregation( + aggregationRaw, + req.determinedLanguage + ) res.send(prepareSeachFilterData(aggregation, filters)) } diff --git a/express/controllers/api/v1/system.js b/express/controllers/api/v1/system.js index 535b48d..c5ce7b4 100644 --- a/express/controllers/api/v1/system.js +++ b/express/controllers/api/v1/system.js @@ -1,9 +1,10 @@ -const debug = require('debug')('termPortal:controllers/api/v1/system') const Eurotermbank = require('../../../models/system/eurotermbank') +// const debug = require('debug')('termPortal:controllers/api/v1/system') exports.handleCspReports = (req, res) => { - debug(req.body) - res.sendStatus(200) + // eslint-disable-next-line no-console + console.error(req.body) + res.end() } exports.syncWithEurotermbank = async (req, res) => { diff --git a/express/controllers/api/v1/users.js b/express/controllers/api/v1/users.js index 406c9a3..6a26ebe 100644 --- a/express/controllers/api/v1/users.js +++ b/express/controllers/api/v1/users.js @@ -1,5 +1,14 @@ +const { randomBytes } = require('crypto') +const { promisify } = require('util') +const RandomBytesAsync = promisify(randomBytes) const User = require('../../../models/user') -const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings') +const email = require('../../../models/email') +const { logout: logoutUser } = require('../../../middleware/user') +const { origin } = require('../../../config/keys') +const { + DEFAULT_HITS_PER_PAGE, + CHANGE_EMAIL_TOKEN_VALID_DAYS +} = require('../../../config/settings') const users = {} users.listUsers = async (req, res) => { @@ -23,13 +32,103 @@ users.updateHitsPerPage = async (req, res) => { res.status(200).send() } -users.updateFristNameAndSurname = async (req, res) => { - const firstname = req.body.name - const surname = req.body.surname +users.updateBasicData = async (req, res) => { + const { firstName, lastName, email: newEmail } = req.body - await User.updateFirstNameAndLastName(req.user.userName, firstname, surname) + // TODO Validation (valid email format, ...). - res.status(200).send() + const oldEmail = await User.updateFirstNameAndLastName( + req.user.id, + firstName, + lastName + ) + + if (newEmail === oldEmail) return res.send() + + if (await User.isEmailAlreadyTaken(newEmail)) { + req.flash('info', req.t('Elektronski naslov uporablja že drug uporabnik.')) + return res.send() + } + + const changeEmailToken = (await RandomBytesAsync(32)).toString('hex') + await User.saveChangeEmailToken(req.user.id, changeEmailToken, newEmail) + let changeEmailLink = new URL('/sprememba-elektronskega-naslova', origin) + changeEmailLink.searchParams.set('token', changeEmailToken) + changeEmailLink = changeEmailLink.href + const renderAsync = promisify(req.app.render.bind(req.app)) + const emailHtml = await renderAsync( + `email/user-change-email-token_${req.language}`, + { + username: req.user.userName, + changeEmailLink + } + ) + await email.send({ + to: newEmail, + subject: req.t('Sprememba elektronskega naslova'), + html: emailHtml + }) + + const message = + req.t( + 'Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ' + ) + + `${CHANGE_EMAIL_TOKEN_VALID_DAYS} ` + + req.t('dni.') + + req.flash('info', message) + res.send() } +users.updatePassword = async (req, res) => { + const { passwordOld, passwordNew, passwordNewRepeat } = req.body + + // TODO Validation (mirror front end validation, ...). + if (passwordNew !== passwordNewRepeat) { + const err = Error(req.t('Gesli se ne ujemata')) + err.status = 403 + err.displayInProd = true + + throw err + } + + await User.changePassword(req.user.id, passwordOld, passwordNew, req.t) + + // TODO Invalidate or log out all session for this user. More details in deleteCurrent method TODO. + + const renderAsync = promisify(req.app.render.bind(req.app)) + const emailHtml = await renderAsync( + `email/user-change-password_${req.language}`, + { + username: req.user.userName + } + ) + await email.send({ + to: req.user.email, + subject: req.t('Sprememba gesla'), + html: emailHtml + }) + + req.flash('info', 'Geslo je bilo spremenjeno.') + res.send() +} + +users.deleteCurrent = [ + async (req, res, next) => { + await User.closeAccount(req.user.id) + next() + }, + logoutUser, + (req, res) => { + // TODO Invalidate or log out all session for this user. Current workaround is in passport.deserializeUser. + // You can probably do it in 1 of 3 ways: + // 1. Brute force; loop through all sessions (using session store's all or ids methods), + // look up their values and remote the ones with user's id + // 2. Include user Id as part of session key; something similar to https://github.com/tj/connect-redis/issues/210#issuecomment-1336545115 + // 3. Create some kind of inverse index, mapping user id to his/hers sessions (also mentioned in issue linked above) + req.flash('info', req.t('Vaš uporabniški račun je bil uspešno izbrisan.')) + res.send() + } +] + module.exports = users diff --git a/express/controllers/consultancy.js b/express/controllers/consultancy.js index 23dda97..9b02b77 100644 --- a/express/controllers/consultancy.js +++ b/express/controllers/consultancy.js @@ -46,11 +46,15 @@ consultancy.search = async (req, res) => { consultancy.specificQuestion = async (req, res) => { const { id } = req.params + // TODO validation: is id of proper format and does a question with it actually exist. + // TODO i18n TIME FORMAT const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id) // const author = await User.fetchUser(entry.authorId) - const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() + const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains( + req.determinedLanguage + ) entry.answerAuthors = entry.answerAuthors.filter(author => author !== '') @@ -86,7 +90,9 @@ consultancy.specificQuestion = async (req, res) => { } consultancy.new = async (req, res) => { - const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() + const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains( + req.determinedLanguage + ) res.locals.isOwnConsultancyEnabled = (await getInstanceSetting('consultancy_type')) === 'own' @@ -110,12 +116,9 @@ consultancyAdmin.new = async (req, res) => { } consultancyAdmin.users = async (req, res) => { - const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() - const users = await User.fetchConsultants() res.render('pages/consultancy/admin/users', { - allPrimaryDomains, users, title: req.t('Svetovalci') }) @@ -175,11 +178,7 @@ consultancyAdmin.inProgress = async (req, res) => { } consultancyAdmin.statistics = async (req, res) => { - const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() - - res.render('pages/consultancy/admin/statistics', { - allPrimaryDomains - }) + res.render('pages/consultancy/admin/statistics') } consultancyAdmin.edit = async (req, res) => { @@ -189,18 +188,11 @@ consultancyAdmin.edit = async (req, res) => { sentFrom[key] = true const moderator = await ConsultancyEntry.getModerator(id) - const editors = await ConsultancyEntry.getEditors(id) - if ( - req.user.hasRole('consultancy admin') || - req.user.hasRole('portal admin') - ) { - console.log('Editor guard omitted due to being administrator') - } else if (editors.filter(editors => editors.id === req.user.id) < 1) { - return res.send('You do not have permsisions to edit this answer') - } // TODO i18n TIME FORMAT const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id) - const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() + const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains( + req.determinedLanguage + ) const author = await User.fetchUser(entry.authorId) const isPublished = entry.status === 'published' @@ -318,7 +310,9 @@ async function consultancyRequest( ) { const searchString = req.query.q?.trim() ?? '' - let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() + let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains( + req.determinedLanguage + ) /// filter selected domains for the prompt /// // Not duplicates of this code arise... diff --git a/express/controllers/demo-paginacija.js b/express/controllers/demo-paginacija.js deleted file mode 100644 index 6b3cdbe..0000000 --- a/express/controllers/demo-paginacija.js +++ /dev/null @@ -1,17 +0,0 @@ -const DemoPaginacija = require('../models/demo-paginacija') -const { DEFAULT_HITS_PER_PAGE } = require('../config/settings') - -const demoPaginacija = {} - -demoPaginacija.izrišiStran = async (req, res) => { - const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE - - const { pages_total: numberOfAllPages, results } = await DemoPaginacija.fetch( - resultsPerPage, - 1 - ) - - res.render('pages/demo-paginacija', { numberOfAllPages, results }) -} - -module.exports = demoPaginacija diff --git a/express/controllers/dictionaries.js b/express/controllers/dictionaries.js index 6370aa3..36b81e6 100644 --- a/express/controllers/dictionaries.js +++ b/express/controllers/dictionaries.js @@ -2,6 +2,7 @@ const { unlink } = require('fs/promises') const { promisify } = require('util') const multer = require('multer') const debug = require('debug')('termPortal:controllers/dictionary') +const user = require('../middleware/user') const Dictionary = require('../models/dictionary') const Entry = require('../models/entry') const User = require('../models/user') @@ -19,6 +20,7 @@ const { const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer') const Extraction = require('../models/extraction') const { isGeneratorFunction } = require('util/types') +const { capitalize } = require('../utils') const importFileBodyParser = multer({ dest: `${DATA_FILES_PATH}/dict_import_temp`, @@ -38,7 +40,10 @@ const dictionary = {} dictionary.list = async (req, res) => { let dictionaries if (req.isAuthenticated()) { - dictionaries = await Dictionary.fetchAllByUser(req.user.id) + dictionaries = await Dictionary.fetchAllByUser( + req.user.id, + req.determinedLanguage + ) } res.render('pages/dictionaries/list', { title: req.t('Seznam slovarjev'), @@ -47,13 +52,11 @@ dictionary.list = async (req, res) => { } dictionary.new = async (req, res) => { - // TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?) - const language = 'name_sl' const [allPrimaryDomains, allSecondaryDomains, allLanguages] = await Promise.all([ - Dictionary.fetchAllPrimaryDomains(), + Dictionary.fetchAllPrimaryDomains(req.determinedLanguage), Dictionary.fetchAllApprovedSecondaryDomains(), - Dictionary.fetchAllLanguages(language, true) + Dictionary.fetchAllLanguages(req.determinedLanguage, true) ]) res.render('pages/dictionaries/new', { @@ -66,7 +69,7 @@ dictionary.new = async (req, res) => { dictionary.create = async (req, res) => { await Dictionary.create(req.body, req.user.id) - res.redirect('/slovarji/moji') + res.redirect(303, '/slovarji/moji') } dictionary.editDescription = async (req, res) => { @@ -77,7 +80,7 @@ dictionary.editDescription = async (req, res) => { dictionary, associatedSecondaryDomains ] = await Promise.all([ - Dictionary.fetchAllPrimaryDomains(), + Dictionary.fetchAllPrimaryDomains(req.determinedLanguage), Dictionary.fetchAllApprovedSecondaryDomains(), Dictionary.fetchEditDescription(dictionaryId), Dictionary.fetchSecondaryDomains(dictionaryId) @@ -103,14 +106,14 @@ dictionary.updateDescription = async (req, res) => { Dictionary.updateSecondaryDomains(dictionaryId, body) ]) - res.redirect('back') + res.redirect(303, 'back') } dictionary.editUsers = async (req, res) => { const dictionaryId = req.params.dictionaryId const [dictionary, userRights, entriesCount, minEntries, publishApproval] = await Promise.all([ - Dictionary.fetchEditUsers(dictionaryId), + Dictionary.fetchEditUsers(dictionaryId, req.determinedLanguage), User.fetchAllWithDictionaryRights(dictionaryId), Dictionary.countPublishedEntries(dictionaryId), getInstanceSetting('min_entries_per_dictionary'), @@ -144,9 +147,9 @@ dictionary.updateUsers = async (req, res) => { const newDictStatus = await determineNewStatus(isPublished) - // TODO I18n - nameSl - const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers( - dictionaryId + const { name, status: oldDictStatus } = await Dictionary.fetchEditUsers( + dictionaryId, + req.determinedLanguage ) await Promise.all([ @@ -165,23 +168,20 @@ dictionary.updateUsers = async (req, res) => { dictionaryId, isPublished, oldDictStatus, - nameSl, + name, req.app, req.user ) - res.redirect('back') + res.redirect(303, 'back') } dictionary.editStructure = async (req, res) => { - // TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?) - // TODO I18n - nameSl - const language = 'name_sl' const { dictionaryId } = req.params const [dictionary, associatedLanguages, allLanguages] = await Promise.all([ Dictionary.fetchEditStructure(dictionaryId), - Dictionary.fetchLanguages(dictionaryId), - Dictionary.fetchAllLanguages(language, true) + Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage), + Dictionary.fetchAllLanguages(req.determinedLanguage, true) ]) let viewPath @@ -211,12 +211,15 @@ dictionary.updateStructure = async (req, res) => { Dictionary.deleteLanguages(dictionaryId), Dictionary.updateLanguages(dictionaryId, body) ]) - res.redirect('back') + res.redirect(303, 'back') } dictionary.editAdvanced = async (req, res) => { const { dictionaryId } = req.params - const dictionaryName = await Dictionary.fetchName(dictionaryId) + const dictionaryName = await Dictionary.fetchName( + dictionaryId, + req.determinedLanguage + ) let viewPath switch (req.baseUrl) { case '/slovarji': @@ -243,7 +246,7 @@ dictionary.comments = async (req, res) => { const [{ comments, pages_total: numberOfAllPages }, dictionaryName] = await Promise.all([ Comment.list(filters, req.user, resultsPerPage, 1), - Dictionary.fetchName(dictionaryId) + Dictionary.fetchName(dictionaryId, req.determinedLanguage) ]) switch (req.baseUrl) { @@ -269,7 +272,7 @@ dictionary.showImportFromFileForm = async (req, res) => { const [{ pages_total: numberOfAllPages, results }, dictionaryName] = await Promise.all([ Dictionary.fetchAllImports(dictionaryId, resultsPerPage, 1), - Dictionary.fetchName(dictionaryId) + Dictionary.fetchName(dictionaryId, req.determinedLanguage) ]) let viewPath switch (req.baseUrl) { @@ -293,7 +296,11 @@ dictionary.listAdminDictionaries = async (req, res) => { const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE const { pages_total: numberOfAllPages, results } = - await Dictionary.fetchAllAdminDictionaries(resultsPerPage, 1) + await Dictionary.fetchAllAdminDictionaries( + req.determinedLanguage, + resultsPerPage, + 1 + ) res.render('pages/admin/dictionaries-list', { title: req.t('Seznam slovarjev'), @@ -311,7 +318,7 @@ dictionary.adminEditDescription = async (req, res) => { associatedSecondaryDomains, status ] = await Promise.all([ - Dictionary.fetchAllPrimaryDomains(), + Dictionary.fetchAllPrimaryDomains(req.determinedLanguage), Dictionary.fetchAllApprovedSecondaryDomains(), Dictionary.fetchEditDescription(dictionaryId), Dictionary.fetchSecondaryDomains(dictionaryId), @@ -357,15 +364,19 @@ dictionary.updateAdminDescription = async (req, res) => { newDictStatus, oldDictStatus, req.app, + req.determinedLanguage, req.user ) - res.redirect('back') + res.redirect(303, 'back') } dictionary.showImportFromExtractionForm = async (req, res) => { const { dictionaryId } = req.params - const dictionaryName = await Dictionary.fetchName(dictionaryId) + const dictionaryName = await Dictionary.fetchName( + dictionaryId, + req.determinedLanguage + ) const extractions = await Extraction.fetchFinishedForUser(req.user.id) let viewPath, title switch (req.baseUrl) { @@ -391,7 +402,7 @@ dictionary.showExportToFileForm = async (req, res) => { const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE const [dictionaryName, { pages_total: numberOfAllPages, results }] = await Promise.all([ - Dictionary.fetchName(dictionaryId), + Dictionary.fetchName(dictionaryId, req.determinedLanguage), Dictionary.fetchExports(dictionaryId, resultsPerPage, 1) ]) let viewPath @@ -418,7 +429,7 @@ dictionary.editDomainLabels = async (req, res) => { const [{ pages_total: numberOfAllPages, results }, dictionaryName] = await Promise.all([ Dictionary.fetchPaginationDomainLabels(dictionaryId, resultsPerPage, 1), - Dictionary.fetchName(dictionaryId) + Dictionary.fetchName(dictionaryId, req.determinedLanguage) ]) let viewPath @@ -445,16 +456,14 @@ dictionary.showContent = async (req, res) => { const [ hits, canPublishEntriesInEdit, - dictionaryName, structure, languages, entryDomainLabels ] = await Promise.all([ searchEntryIndex(hitsQuery), getInstanceSetting('can_publish_entries_in_edit'), - Dictionary.fetchName(dictionaryId), Dictionary.fetchEditStructure(dictionaryId), - Dictionary.fetchLanguages(dictionaryId), + Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage), Dictionary.fetchDomainLabels(dictionaryId) ]) @@ -464,7 +473,7 @@ dictionary.showContent = async (req, res) => { title: req.t('Vsebina slovarja'), terms, canPublishEntriesInEdit, - dictionaryName, + dictionaryName: structure[`name${capitalize(req.determinedLanguage)}`], structure, languages, dictionaryId, @@ -510,7 +519,8 @@ dictionary.dictionaryList = async (req, res) => { hitsPerPage, page, orderAttribute, - orderIndex + orderIndex, + req.determinedLanguage ) const numberOfAllHits = parseInt( @@ -518,7 +528,9 @@ dictionary.dictionaryList = async (req, res) => { ) const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage) - const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() + const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains( + req.determinedLanguage + ) /* /// filter selected domains for the prompt /// @@ -567,9 +579,12 @@ dictionary.dictionaryDetails = async (req, res) => { targetLanguages, allDictionaryNames, portals - } = await SFDSuggestionImporter.initialize() + } = await SFDSuggestionImporter.initialize(req.determinedLanguage) - const dictionaryData = await Dictionary.fetchDictionaryBasicInfo(dictId) + const dictionaryData = await Dictionary.fetchDictionaryBasicInfo( + dictId, + req.determinedLanguage + ) const filters = { ctxType: 'dictionary', ctxId: dictId } // TODO: integrate numberOfAllPages, commentCount with pug @@ -711,21 +726,34 @@ dictionary.importFromFile = async (req, res) => { } } -dictionary.exportDownload = async (req, res) => { - // TODO Add authentication and authorization. +dictionary.exportDownload = [ + async (req, res, next) => { + const { exportId } = req.params + const exportDownloadMetadata = await Dictionary.fetchExportDownloadMetadata( + exportId + ) - const { exportId } = req.params - const { exportStatus, dictionaryId, nameString, timeString, fileFormat } = - await Dictionary.fetchExportDownloadMetadata(exportId) - if (exportStatus !== 'finished') { - throw Error("Can't request file for unfinished export") + req.dictionaryId = exportDownloadMetadata.dictionaryId + req.exportDownloadMetadata = exportDownloadMetadata + + next() + }, + user.isDictionaryEditor, + (req, res) => { + const { exportId } = req.params + const { exportStatus, dictionaryId, nameString, timeString, fileFormat } = + req.exportDownloadMetadata + + if (exportStatus !== 'finished') { + throw Error("Can't request file for unfinished export") + } + const exportFilesPath = getExportFilesPath(dictionaryId) + const exportFilePath = `${exportFilesPath}/${exportId}` + const exportFileName = `${nameString}_${timeString}.${fileFormat}` + + res.download(exportFilePath, exportFileName) } - const exportFilesPath = getExportFilesPath(dictionaryId) - const exportFilePath = `${exportFilesPath}/${exportId}` - const exportFileName = `${nameString}_${timeString}.${fileFormat}` - - res.download(exportFilePath, exportFileName) -} +] function importFileFilter(req, file, cb) { if (file.mimetype !== 'text/xml') { diff --git a/express/controllers/extraction-poc.js b/express/controllers/extraction-poc.js deleted file mode 100644 index 0674127..0000000 --- a/express/controllers/extraction-poc.js +++ /dev/null @@ -1,150 +0,0 @@ -const { mkdir } = require('fs/promises') -const { - getDocumentsPath, - getStopTermsPath -} = require('../models/helpers/extraction') -const { checkIfcanBegin } = require('./helpers/extraction') -const { MAX_EXTRACTIONS_PER_USER } = require('../config/settings') -const Extraction = require('../models/extraction') -const Dictionary = require('../models/dictionary') -const Domain = require('../models/domain') -const { intoDbArray } = require('../models/helpers') -const { DEFAULT_HITS_PER_PAGE } = require('../config/settings') - -const extraction = {} - -extraction.list = async (req, res) => { - let extractions = await Extraction.fetchAllForUser(req.user.id) - extractions = await Promise.all( - extractions.map(async extraction => { - extraction.canBegin = await checkIfcanBegin(extraction) - if (extraction.status === 'finished') { - extraction.termCandidatesCount = - await Extraction.fetchTermCandidatesCount(extraction.id) - } - return extraction - }) - ) - res.render('extraction-poc/list', { extractions }) -} - -extraction.create = async (req, res) => { - const extractionCount = await Extraction.countAllForUser(req.user.id) - if (extractionCount >= MAX_EXTRACTIONS_PER_USER) { - // TODO Tukaj bo treba prikazati tudi obvestilo uporabniku skladno s trenutno metodologijo prikaza obvestil. - return res.redirect(303, 'back') - } - - const extractionName = `Luščenje ${extractionCount + 1}` - const { extractionType } = req.body - - let extractionId - if (extractionType === 'own') { - extractionId = await Extraction.createOwn(req.user.id, extractionName) - - const documentsPath = getDocumentsPath(extractionId) - const stopTermsPath = getStopTermsPath(extractionId) - await Promise.all([ - mkdir(documentsPath, { recursive: true }), - mkdir(stopTermsPath, { recursive: true }) - ]) - } else { - extractionId = await Extraction.createOss(req.user.id, extractionName) - - const stopTermsPath = getStopTermsPath(extractionId) - await mkdir(stopTermsPath, { recursive: true }) - } - - // Redirect to extraction edit page. - res.redirect(`poc/${extractionId}`) -} - -extraction.edit = async (req, res) => { - const extractionId = req.params.id - const extraction = await Extraction.fetch(extractionId) - - if (extraction.ossParams) { - const [allPrimaryDomains, stopTermsFiles] = await Promise.all([ - Dictionary.fetchAllPrimaryDomains(), - Extraction.fetchAllStopTermsFilesStats(extractionId) - ]) - const { params } = extraction.ossParams - const domainUdk = params?.domainUdk?.[0] - if (domainUdk) - extraction.domainId = await Domain.fetchIdByUdkCode(domainUdk) - extraction.documentType = intoDbArray(params.documentType, 'always') - extraction.year = intoDbArray(params.year, 'always') - extraction.keywords = intoDbArray(params.keywords, 'always') - - res.render('extraction-poc/edit-oss', { - id: extractionId, - extraction, - allPrimaryDomains, - stopTermsFiles - }) - } else { - const [extractionDocuments, stopTermsFiles] = await Promise.all([ - Extraction.fetchAllDocumentsStats(extractionId), - Extraction.fetchAllStopTermsFilesStats(extractionId) - ]) - res.render('extraction-poc/edit-own', { - id: extractionId, - extraction, - extractionDocuments, - stopTermsFiles - }) - } -} - -extraction.updateOwn = async (req, res) => { - const extractionId = req.params.id - await Extraction.update(extractionId, req.body.name) - - // Reload page. - res.redirect(`./${extractionId}`) -} - -extraction.docsEdit = async (req, res) => { - const extractionId = req.params.id - const extractionDocuments = await Extraction.fetchAllDocumentsStats( - extractionId - ) - - res.render('extraction-poc/docs-edit', { - id: extractionId, - extractionDocuments - }) -} - -extraction.stopTermsEdit = async (req, res) => { - const extractionId = req.params.id - const stopTermsFiles = await Extraction.fetchAllStopTermsFilesStats( - extractionId - ) - - res.render('extraction-poc/stop-terms-edit', { - id: extractionId, - stopTermsFiles - }) -} - -extraction.listTermCandidates = async (req, res) => { - const extractionId = req.params.id - const termCandidatesJson = await Extraction.fetchTermCandidatesJson( - extractionId - ) - const termCandidates = JSON.parse(termCandidatesJson).terminoloski_kandidati - const hitsPerPage = +req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE - const numberOfAllPages = Math.ceil(termCandidates.length / hitsPerPage) - const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage) - - res.render('extraction-poc/term-candidates', { - extractionId, - termCandidatesJson, - firstPageOfTermCandidates, - hitsPerPage, - numberOfAllPages - }) -} - -module.exports = extraction diff --git a/express/controllers/extraction.js b/express/controllers/extraction.js index 1174d33..d7fc1f7 100644 --- a/express/controllers/extraction.js +++ b/express/controllers/extraction.js @@ -63,22 +63,36 @@ extraction.create = async (req, res) => { } // Redirect to extraction edit page. - res.redirect(`luscenje/${extractionId}`) + res.redirect(303, `luscenje/${extractionId}`) +} + +extraction.validateOwnership = async (req, res, next) => { + const { id: extractionId } = req.params + if (!extractionId) return res.redirect(303, '/') + + const extraction = await Extraction.fetch(extractionId) + if (extraction.userId !== req.user.id) return res.redirect(303, '/') + + req.extractionData = extraction + next() } extraction.edit = async (req, res) => { const extractionId = req.params.id - const extraction = await Extraction.fetch(extractionId) + const extraction = req.extractionData if (extraction.ossParams) { - const [allPrimaryDomains, stopTermsFiles] = await Promise.all([ - Dictionary.fetchAllPrimaryDomains(), - Extraction.fetchAllStopTermsFilesStats(extractionId) - ]) + const [allPrimaryDomains, ossDocumentTypes, stopTermsFiles] = + await Promise.all([ + Dictionary.fetchAllPrimaryDomains(req.determinedLanguage), + Extraction.fetchOssDocumentTypes(req.determinedLanguage), + Extraction.fetchAllStopTermsFilesStats(extractionId) + ]) const { params } = extraction.ossParams const domainUdk = params?.domainUdk?.[0] - if (domainUdk) + if (domainUdk) { extraction.domainId = await Domain.fetchIdByUdkCode(domainUdk) + } extraction.documentType = intoDbArray(params.documentType, 'always') extraction.year = intoDbArray(params.year, 'always') extraction.keywords = intoDbArray(params.keywords, 'always') @@ -88,6 +102,7 @@ extraction.edit = async (req, res) => { id: extractionId, extraction, allPrimaryDomains, + ossDocumentTypes, stopTermsFiles }) } else { @@ -110,7 +125,7 @@ extraction.updateOwn = async (req, res) => { await Extraction.update(extractionId, req.body.name) // Reload page. - res.redirect(`./${extractionId}`) + res.redirect(303, `./${extractionId}`) } extraction.docsEdit = async (req, res) => { diff --git a/express/controllers/helpers/dictionary.js b/express/controllers/helpers/dictionary.js index fbca42a..fc3c69a 100644 --- a/express/controllers/helpers/dictionary.js +++ b/express/controllers/helpers/dictionary.js @@ -42,7 +42,7 @@ const minEntriesEmailBookmark = { // Exports actions related to checking and acting on minimum entries per dictionary setting. exports.minEntriesRequirementCheckAndAct = { // Checks required criteria and sends notifications emails if required. - async onDelete(dictionaryId, appRef) { + async onDelete(dictionaryId, appRef, determinedLanguage) { const minEntries = await getInstanceSetting('min_entries_per_dictionary') // Only proceed if a valid and positive minimum entries per dictionary setting is set. @@ -58,9 +58,8 @@ exports.minEntriesRequirementCheckAndAct = { if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return // Prepare and send notification emails. - // TODO I18n - nameSl - const [nameSl, adminEmails, dictionariesAdminEmails] = await Promise.all([ - Dictionary.fetchName(dictionaryId), + const [name, adminEmails, dictionariesAdminEmails] = await Promise.all([ + Dictionary.fetchName(dictionaryId, determinedLanguage), Dictionary.fetchAdminEmails(dictionaryId), Dictionary.fetchDictionariesAdminEmails() ]) @@ -71,10 +70,9 @@ exports.minEntriesRequirementCheckAndAct = { const type = 'delete' const renderAsync = promisify(appRef.render.bind(appRef)) - // TODO I18n - nameSl const emailHtml = await renderAsync('email/dictionary-status-change', { type, - nameSl + name }) // TODO i18n - What language are the email title and content (we already have email translated) @@ -123,12 +121,11 @@ exports.determineNewStatus = async isPublished => { // Exports actions related to checking and acting on dictionary status changes. exports.statusChangeCheckAndAct = { // Notify dictionaries admins by email on dictionary status changes. - // TODO I18n - nameSl async updateUsers( dictionaryId, isPublishedNew, oldDictStatus, - nameSl, + name, appRef, user ) { @@ -139,12 +136,11 @@ exports.statusChangeCheckAndAct = { const dictionariesAdminEmails = await Dictionary.fetchDictionariesAdminEmails() const type = 'unpublish' - // TODO I18n - nameSl await renderAndSendStatusChangeEmails( appRef, type, user.email, - nameSl, + name, dictionariesAdminEmails ) @@ -157,12 +153,11 @@ exports.statusChangeCheckAndAct = { const type = isApprovalRequired === 'T' ? 'publish-approval' : 'publish-no-approval' - // TODO I18n - nameSl await renderAndSendStatusChangeEmails( appRef, type, user.email, - nameSl, + name, dictionariesAdminEmails ) } @@ -175,12 +170,12 @@ exports.statusChangeCheckAndAct = { statusNew, statusOld, appRef, + determinedLanguage, user ) { if (statusOld === 'reviewed' && statusNew !== 'reviewed') { - // TODO I18n - nameSl - const [nameSl, adminEmails] = await Promise.all([ - Dictionary.fetchName(dictionaryId), + const [name, adminEmails] = await Promise.all([ + Dictionary.fetchName(dictionaryId, determinedLanguage), Dictionary.fetchAdminEmails(dictionaryId) ]) const type = 'status' @@ -189,7 +184,7 @@ exports.statusChangeCheckAndAct = { appRef, type, user.email, - nameSl, + name, adminEmails ) } @@ -197,19 +192,18 @@ exports.statusChangeCheckAndAct = { } // Helper function used by statusChangeCheckAndAct methods. -// TODO I18n - nameSl async function renderAndSendStatusChangeEmails( appRef, type, changerEmail, - nameSl, + name, targetEmails ) { const renderAsync = promisify(appRef.render.bind(appRef)) const emailHtml = await renderAsync('email/dictionary-status-change', { type, changerEmail, - nameSl + name }) // TODO i18n - What language are the email title and content (we already have email translated) diff --git a/express/controllers/helpers/search-filter-data-suggestion-importer.js b/express/controllers/helpers/search-filter-data-suggestion-importer.js index 2581cb7..817d021 100644 --- a/express/controllers/helpers/search-filter-data-suggestion-importer.js +++ b/express/controllers/helpers/search-filter-data-suggestion-importer.js @@ -4,24 +4,23 @@ const { getInstanceSetting } = require('../../models/helpers') const helper = {} -helper.initialize = async () => { +helper.initialize = async determinedLanguage => { const initializers = {} - // TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?) - // TODO i18n name_sl - const language = 'name_sl' - // TODO Consider parallelizing following queries. Single vs pooled clients? - initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains() - initializers.sourceLanguages = await Dictionary.fetchAllLanguages(language) - initializers.targetLanguages = ( - await Dictionary.fetchAllLanguages(language) - ).filter( + initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains( + determinedLanguage + ) + const allLanguages = await Dictionary.fetchAllLanguages(determinedLanguage) + initializers.sourceLanguages = allLanguages + initializers.targetLanguages = allLanguages.filter( // drop slovene language l => l.id !== 32 ) - initializers.allDictionaryNames = await Dictionary.fetchAll() + initializers.allDictionaryNames = await Dictionary.fetchAll( + determinedLanguage + ) initializers.portals = [] initializers.portals.push({ diff --git a/express/controllers/index.js b/express/controllers/index.js index 53044d2..c8a4471 100644 --- a/express/controllers/index.js +++ b/express/controllers/index.js @@ -1,3 +1,4 @@ +const { promisify } = require('util') const Entry = require('../models/entry') const { getInstanceSetting } = require('../models/helpers') const Dictionary = require('../models/dictionary') @@ -13,10 +14,12 @@ const { prepareSeachFilterData, prepareConsultancyEntries } = require('../models/helpers/search') +const email = require('../models/email') const generateQuery = require('../models/helpers/search/generate-query') const { DEFAULT_HITS_PER_PAGE } = require('../config/settings') const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer') const User = require('../models/user') +const { capitalize } = require('../utils') // TODO Luka (note to self): Measure performance, consider caching. exports.index = async (req, res) => { @@ -27,12 +30,10 @@ exports.index = async (req, res) => { targetLanguages, allDictionaryNames, portals - } = await SFDSuggestionImporter.initialize() - - const englishLanguageEnabled = false // dummy variable for future edit + } = await SFDSuggestionImporter.initialize(req.determinedLanguage) const latestDicts = await Dictionary.fetchLatest3DictsByPublishDate( - englishLanguageEnabled + req.determinedLanguage ) const portalName = await getInstanceSetting(`portal_name_${language}`) @@ -58,7 +59,7 @@ exports.index = async (req, res) => { exports.search = async (req, res) => { const searchString = req.query.q?.trim() - if (!searchString) return res.redirect('/') + if (!searchString) return res.redirect(303, '/') const title = req.t('Iskanje') @@ -68,7 +69,7 @@ exports.search = async (req, res) => { targetLanguages, allDictionaryNames, portals - } = await SFDSuggestionImporter.initialize() + } = await SFDSuggestionImporter.initialize(req.determinedLanguage) const filters = { sourceLanguages: intoDbArray(req.query.sl, 'always'), @@ -131,7 +132,10 @@ exports.search = async (req, res) => { // Similar to search query without any extra filters, this is required for modal to diplay ALL res. // const allAggregation = await indexAllResultsNoFiltering(req, res) - const aggregation = await prepareAggregation(aggregationRaw) + const aggregation = await prepareAggregation( + aggregationRaw, + req.determinedLanguage + ) const searchFilterData = prepareSeachFilterData(aggregation, filters) // TODO Luka (note to self): Consider reworking Miha's logic below. It's hacky and possibly error prone. @@ -328,7 +332,7 @@ exports.entryDetails = async (req, res) => { // If external url, just redirect if (entry.external_url) { - return res.redirect(entry.external_url) + return res.redirect(303, entry.external_url) } // unnecessary legacy assigment, refactor when time is available @@ -340,12 +344,15 @@ exports.entryDetails = async (req, res) => { targetLanguages, allDictionaryNames, portals - } = await SFDSuggestionImporter.initialize() + } = await SFDSuggestionImporter.initialize(req.determinedLanguage) const [dictStruct, dictionaryData, selectedDomainLabelsForEntry] = await Promise.all([ Dictionary.fetchDictionaryWithEditStructure(entry.dictionary_id), - Dictionary.fetchDictionaryBasicInfo(entry.dictionary_id), + Dictionary.fetchDictionaryBasicInfo( + entry.dictionary_id, + req.determinedLanguage + ), Entry.fetchDomainLabels(termId) ]) @@ -399,7 +406,7 @@ exports.entryDetails = async (req, res) => { prevHref: '/iskanje', portalCode: dictionaryData[0].portalcode, portalName: dictionaryData[0].portalname, - dictName: structure.nameSl, + dictName: structure[`name${capitalize(req.determinedLanguage)}`], dictHref: `/slovarji/${structure.id}/o-slovarju?sentFromEntryId=${termId}`, fullAuthorName: reducedData.author ? reducedData.author.join(', ') : '', areas: dictionaryData[0].domain_primary, @@ -438,11 +445,13 @@ exports.entryDetails = async (req, res) => { entry.foreignEntries[idx] = await }) */ - const langs = await Dictionary.fetchLanguages(entryData.dictionary_id) + const langs = await Dictionary.fetchLanguages( + entryData.dictionary_id, + req.determinedLanguage + ) entryData.foreign_entries.forEach((val, idx) => { try { - entry.foreign_entries[idx].name_sl = langs[idx].nameSl - entry.foreign_entries[idx].name_en = langs[idx].nameEn + entry.foreign_entries[idx].name = langs[idx].name } catch (e) {} }) @@ -479,16 +488,44 @@ exports.changePassword = async (req, res) => { } exports.resetPassword = async (req, res) => { + if (req.user) return res.redirect(303, '/spremeni-geslo') + const { token } = req.query + if (!token) return res.redirect(303, '/') + + const isTokenValid = await User.isResetPasswordTokenValid(token) - // console.log(token) res.render('pages/reset-password/reset-password', { - // title: 'Pozabljeno geslo' - isValidToken: token === '123', + title: 'Ponastavitev gesla', + isTokenValid, token }) } +exports.changeEmail = async (req, res) => { + const { token } = req.query + if (!token) return res.redirect(303, '/') + + const user = await User.changeEmailWithToken(token, req.t) + + const renderAsync = promisify(req.app.render.bind(req.app)) + const emailHtml = await renderAsync( + `email/user-change-email-success_${req.language}`, + { + username: user.username + } + ) + await email.send({ + to: user.email, + subject: req.t('Sprememba elektronskega naslova - uspeh'), + html: emailHtml + }) + + req.flash('info', req.t('Uspešno ste spremenili svoj elektronski naslov.')) + if (req.user) return res.redirect(303, '/moj-racun') + res.redirect(303, '/') +} + exports.userSettings = async (req, res) => { const hitsPerPageArr = await User.fetchAllowedHitsPerPage() res.render('pages/profile/change-profile-settings', { @@ -513,7 +550,7 @@ exports.changeUserLanguage = async (req, res) => { req.session.language = languageCode } - res.redirect('/') + res.redirect(303, '/') } function mergeDomains( diff --git a/express/controllers/portals.js b/express/controllers/portals.js index af79e33..6d0b29d 100644 --- a/express/controllers/portals.js +++ b/express/controllers/portals.js @@ -17,7 +17,7 @@ portal.updateInstaceSettings = async (req, res) => { const payload = req.body await Portal.updateInstaceSettings(payload) await clearCachedInstanceSettings() - res.redirect('/admin/nastavitve/portal') + res.redirect(303, '/admin/nastavitve/portal') } portal.instanceDictSettings = async (req, res) => { @@ -33,7 +33,7 @@ portal.updateInstanceDictSettings = async (req, res) => { const payload = req.body await Portal.updateInstaceDictSettings(payload) await clearCachedInstanceSettings() - res.redirect('/admin/nastavitve/slovarji') + res.redirect(303, '/admin/nastavitve/slovarji') } portal.instanceConsultancySettings = async (req, res) => { @@ -48,7 +48,7 @@ portal.updateInstanceConusltacySettings = async (req, res) => { const payload = req.body await Portal.updateInstaceConsultancySettings(payload) await clearCachedInstanceSettings() - res.redirect('/admin/nastavitve/svetovalnica') + res.redirect(303, '/admin/nastavitve/svetovalnica') } portal.new = async (req, res) => { @@ -80,7 +80,7 @@ portal.updatePortal = async (req, res) => { const portalId = req.params.portalId const payload = req.body await Portal.update(portalId, payload) - res.redirect('/admin/povezave/seznam') + res.redirect(303, '/admin/povezave/seznam') } portal.fetchSelectedLinkedDictionaries = async (req, res) => { @@ -101,7 +101,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => { portal.updateSelectedDictionaries = async (req, res) => { const linkedId = req.params.portalId await Portal.updateSelectedDictionaries(linkedId, req.body) - res.redirect('back') + res.redirect(303, 'back') } portal.fetchAllLinkedDictionaries = async (req, res) => { @@ -119,7 +119,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => { portal.updateAllDictionaries = async (req, res) => { await Portal.updateAllDictionaries(req.body) - res.redirect('back') + res.redirect(303, 'back') } portal.comments = async (req, res) => { diff --git a/express/controllers/users.js b/express/controllers/users.js index 5602e1e..df9ad8f 100644 --- a/express/controllers/users.js +++ b/express/controllers/users.js @@ -3,6 +3,7 @@ const { promisify } = require('util') const passport = require('passport') const User = require('../models/user') const email = require('../models/email') +const { logout: logoutUser } = require('../middleware/user') const { origin } = require('../config/keys') const { rememberMeCookieSettings } = require('../config/settings') const RandomBytesAsync = promisify(randomBytes) @@ -12,10 +13,14 @@ const user = {} user.register = async (req, res) => { // TODO Add validation. - const userId = await User.create({ - ...req.body, - language: req.session.language - }) + + const userId = await User.create( + { + ...req.body, + language: req.session.language + }, + req.t + ) const activationToken = (await RandomBytesAsync(32)).toString('hex') await User.saveActivationToken(userId, activationToken) const { email: userEmail, username } = req.body @@ -38,8 +43,9 @@ user.register = async (req, res) => { user.activateAccount = async (req, res) => { // TODO Add validation. What if user is already logged in? What if account is already active? ... const { token } = req.query - const user = await User.fetchByActivationToken(token) - await User.activateAccount(user) + if (!token) return res.redirect(303, '/') + + const user = await User.activateAccountWithToken(token, req.t) const loginAsync = promisify(req.login.bind(req)) await loginAsync(user) @@ -48,7 +54,11 @@ user.activateAccount = async (req, res) => { delete req.session.language } - res.redirect('/') + req.flash( + 'info', + req.t('Uspešno ste aktivirali svoj uporabniški račun in se prijavili.') + ) + res.redirect(303, '/') } user.login = async (req, res, next) => { @@ -89,20 +99,86 @@ user.login = async (req, res, next) => { )(req, res, next) } -user.logout = async (req, res) => { - const rememberMeToken = req.signedCookies.remember_me +user.logout = [logoutUser, (req, res) => res.redirect(303, '/')] - if (rememberMeToken) { - res.clearCookie('remember_me') - await User.clearRememberMeToken(rememberMeToken) +user.generateResetPasswordToken = async (req, res) => { + const { usernameOrEmail } = req.body + const user = await User.fetchByUsernameOrEmail(usernameOrEmail) + + if (!user) { + return res + .status(400) + .send(req.t('Nepravilno uporabniško ime ali elektronski naslov.')) } - req.session.language = req.user.language + if (user.status !== 'active') { + return res + .status(400) + .send( + req.t( + 'Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.' + ) + ) + } - req.logout() - // Manually clear session.passport due to bug in current passport version. - delete req.session.passport.user - res.redirect('/') + const resetPasswordToken = (await RandomBytesAsync(32)).toString('hex') + await User.saveResetPasswordToken(user.id, resetPasswordToken) + const { email: userEmail, username } = user + let resetPasswordLink = new URL('/ponastavitev-gesla', origin) + resetPasswordLink.searchParams.set('token', resetPasswordToken) + resetPasswordLink = resetPasswordLink.href + const renderAsync = promisify(req.app.render.bind(req.app)) + // TODO i18n - prepare proper slovenian an english email templates + const emailHtml = await renderAsync( + `email/user-reset-password-token_${req.language}`, + { + username, + resetPasswordLink + } + ) + await email.send({ + to: userEmail, + subject: req.t('Ponastavitev gesla'), + html: emailHtml + }) + res.send() +} + +user.changePassword = async (req, res) => { + const { token, password, passwordRepeat } = req.body + + if (password !== passwordRepeat) { + return res.status(400).send(req.t('Gesli se ne ujemata')) + } + // TODO Add additional password validation (min length, ...) + + const user = await User.resetPasswordWithToken(token, password, req.t) + + const renderAsync = promisify(req.app.render.bind(req.app)) + // TODO i18n - prepare proper slovenian an english email templates + const emailHtml = await renderAsync( + `email/user-reset-password-success_${req.language}`, + { + username: user.username + } + ) + await email.send({ + to: user.email, + subject: req.t('Uspešna ponastavitev gesla'), + html: emailHtml + }) + + req.flash('info', req.t('Vaše geslo je bilo uspešno ponastavljeno.')) + + const loginAsync = promisify(req.login.bind(req)) + await loginAsync(user) + + if (req.session.language) { + await User.updateLanguage(user.id, req.session.language) + delete req.session.language + } + + res.send() } user.list = async (req, res) => { @@ -135,7 +211,7 @@ user.findByUsernameOrEmail = async (req, res) => { user.updateRoles = async (req, res) => { await User.updatePortalRoles(req.body.rolesPerUser) - res.redirect('back') + res.redirect(303, 'back') } user.adminEdit = async (req, res) => { @@ -144,6 +220,9 @@ user.adminEdit = async (req, res) => { User.fetchUser(userId), User.fetchUserRoles(userId) ]) + + if (userData.status === 'closed') return res.redirect(303, '/') + res.render('pages/admin/user-edit', { title: req.t('Uporabnik'), userData, @@ -156,7 +235,7 @@ user.adminEdit = async (req, res) => { user.adminUpdate = async (req, res) => { const { userId } = req.params await User.updateUser(userId, req.body) - res.redirect('back') + res.redirect(303, 'back') } module.exports = user diff --git a/express/middleware/auth.js b/express/middleware/auth.js index b8fc94f..5cd0fcc 100644 --- a/express/middleware/auth.js +++ b/express/middleware/auth.js @@ -21,6 +21,9 @@ passport.deserializeUser(async (id, done) => { const user = await User.fetchDeserializedDataById(id) // TODO Consider what to do if no user was found? + + // TODO This is a current workaround until session invalidation is implemented. + if (user.status !== 'active') return done(null, false) done(null, user) } catch (error) { done(error) @@ -32,11 +35,7 @@ passport.use( { passReqToCallback: true, usernameField: 'usernameOrEmail' }, async (req, usernameOrEmail, password, done) => { try { - const { rows } = await db.query( - 'SELECT id, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1', - [usernameOrEmail] - ) - const user = rows[0] + const user = await User.fetchByUsernameOrEmail(usernameOrEmail) if (!user) { return done(null, false, { diff --git a/express/middleware/index.js b/express/middleware/index.js index b7a1730..284f348 100644 --- a/express/middleware/index.js +++ b/express/middleware/index.js @@ -2,6 +2,13 @@ const { getInstanceSetting } = require('../models/helpers') exports.enhanceLocals = async (req, res, next) => { res.locals.portalCode = await getInstanceSetting('portal_code') + res.locals.flashInfo = req.flash('info') + + next() +} + +exports.adjustHeaders = async (req, res, next) => { + res.set('Cache-Control', 'no-cache, private') next() } diff --git a/express/middleware/user.js b/express/middleware/user.js index 7ad13d5..19ae856 100644 --- a/express/middleware/user.js +++ b/express/middleware/user.js @@ -1,3 +1,5 @@ +const User = require('../models/user') + const user = {} user.enhance = (req, res, next) => { @@ -18,7 +20,16 @@ user.isAuthenticated = (req, res, next) => { if (req.isAuthenticated()) return next() if (req.isAjax) return res.status(400).end() - res.redirect(req.baseUrl || '/') + res.redirect(303, req.baseUrl || '/') +} + +user.isPortalAdmin = (req, res, next) => { + const isPortalAdmin = req.user.hasRole('portal admin') + + if (isPortalAdmin) return next() + + if (req.isAjax) return res.status(400).end() + res.redirect(303, req.baseUrl || '/') } user.isDictionaryAdmin = (req, res, next) => { @@ -28,17 +39,37 @@ user.isDictionaryAdmin = (req, res, next) => { if (isAdmin) return next() if (req.isAjax) return res.status(400).end() - res.redirect(req.baseUrl || '/') + res.redirect(303, req.baseUrl || '/') } user.isDictionaryEditor = (req, res, next) => { - const { dictionaryId } = req.params + const dictionaryId = req.params.dictionaryId || req.dictionaryId const isEditor = req.user.hasAnyDictionaryRole(dictionaryId) if (isEditor) return next() if (req.isAjax) return res.status(400).end() - res.redirect(req.baseUrl || '/') + res.redirect(303, req.baseUrl || '/') +} + +user.canAdministrateDictionary = (req, res, next) => { + const dictionaryId = req.params.dictionaryId || req.dictionaryId + const isAdmin = req.user.hasDictionaryRole(dictionaryId, 'administration') + const isPortalAdmin = req.user.hasRole('portal admin') + const isDictionariesAdmin = req.user.hasRole('dictionaries admin') + if (isAdmin || isPortalAdmin || isDictionariesAdmin) return next() + + if (req.isAjax) return res.status(400).end() + res.redirect(303, req.baseUrl || '/') +} + +user.canAdministrateDictionaries = (req, res, next) => { + const isPortalAdmin = req.user.hasRole('portal admin') + const isDictionariesAdmin = req.user.hasRole('dictionaries admin') + if (isPortalAdmin || isDictionariesAdmin) return next() + + if (req.isAjax) return res.status(400).end() + res.redirect(303, req.baseUrl || '/') } user.canContentEdit = (req, res, next) => { @@ -49,7 +80,54 @@ user.canContentEdit = (req, res, next) => { if (isEditor || isPortalAdmin || isDictionariesAdmin) return next() if (req.isAjax) return res.status(400).end() - res.redirect(req.baseUrl || '/') + res.redirect(303, req.baseUrl || '/') +} + +user.canAdministrateConsultancy = (req, res, next) => { + const isPortalAdmin = req.user.hasRole('portal admin') + const isConsultancyAdmin = req.user.hasRole('consultancy admin') + if (isPortalAdmin || isConsultancyAdmin) return next() + + if (req.isAjax) return res.status(400).end() + res.redirect(303, req.baseUrl || '/') +} + +user.canConsult = (req, res, next) => { + const isConsultant = req.user.hasRole('consultant') + const isPortalAdmin = req.user.hasRole('portal admin') + const isConsultancyAdmin = req.user.hasRole('consultancy admin') + if (isConsultant || isPortalAdmin || isConsultancyAdmin) return next() + + if (req.isAjax) return res.status(400).end() + res.redirect(303, req.baseUrl || '/') +} + +user.canConsultEntry = (req, res, next) => { + const id = req.params.id || req.entryId + const isConsultantForEntry = req.user.isEditorOfConsultancyEntry(id) + const isPortalAdmin = req.user.hasRole('portal admin') + const isConsultancyAdmin = req.user.hasRole('consultancy admin') + if (isConsultantForEntry || isPortalAdmin || isConsultancyAdmin) return next() + + if (req.isAjax) return res.status(400).end() + res.redirect(303, req.baseUrl || '/') +} + +user.logout = async (req, res, next) => { + const rememberMeToken = req.signedCookies.remember_me + + if (rememberMeToken) { + res.clearCookie('remember_me') + await User.clearRememberMeToken(rememberMeToken) + } + + req.session.language = req.user.language + + req.logout() + // Manually clear session.passport due to bug in current passport version. + delete req.session.passport.user + + next() } /** diff --git a/express/models/comment.js b/express/models/comment.js index 5547f2d..adba96c 100644 --- a/express/models/comment.js +++ b/express/models/comment.js @@ -1,10 +1,6 @@ const db = require('./db') -const debug = require('debug')('termPortal:models/comment') -const User = require('../models/user') -const { - portalAdminInitialEmail, - portalAdminInitialPassword -} = require('../config/keys') +const Entry = require('./entry') +// const debug = require('debug')('termPortal:models/comment') class Comment { // Deserialize flat data into an organized comment object. @@ -84,15 +80,18 @@ class Comment { } break - case 'entry_dict_ext': + case 'entry_dict_ext': { + const { dictionary_id: dictionaryId } = await Entry.fetch(ctxId) + if ( user.hasRole('portal admin') || - user.hasRole('consultancy admin') || - user.hasDictionaryRole(ctxId, 'administration') + user.hasRole('dictionaries admin') || + user.hasDictionaryRole(dictionaryId, 'administration') ) { isCommentModerator = true } break + } default: throw Error('Invalid context type') @@ -189,6 +188,18 @@ class Comment { await db.query(text, values) } + // Fetch context info for a specific comment. + static async fetchContextById(id) { + const { + rows: [{ context_type: ctxType, context_id: ctxId }] + } = await db.query( + 'SELECT context_type, context_id FROM comment WHERE id = $1', + [id] + ) + + return { ctxType, ctxId } + } + static async updateStatus(status, id) { const values = [id, status] const text = ` @@ -197,237 +208,6 @@ class Comment { WHERE id = $2` await db.query(text, values) } - - // Insert a new demo comment into DB. - static async createDemo(comment) { - const text = - "INSERT INTO comment (message, author_id, context_type, quoted_comment_id) VALUES ($1, $2, 'portal', $3) RETURNING id" - const values = [comment.message, pickRandomMockUserId(), comment.quoteId] - const { rows } = await db.query(text, values) - const idOfInsertedComment = rows[0].id - const text2 = `${selectAllCommentsQueryString} WHERE c.id = ${idOfInsertedComment}` - const { rows: rows2 } = await db.query(text2) - - const insertedComment = rows2[0] - - const deserializedComment = new this(insertedComment) - - return deserializedComment - } - - // Seed DB with random comments. - static async seed(commentCount) { - const seedTasks = [] - - for (let i = 0; i < commentCount; i++) { - seedTasks.push(this.createDemo({ message: pickRandomMockMessage() })) - } - - const seededComments = await Promise.all(seedTasks) - debug(`Successfully seeded ${commentCount} comments`) - debug('Comments:') - seededComments.forEach(comment => debug(comment)) - } - - // Clear all comments from DB. - static async clear() { - await db.query('TRUNCATE comment') - } -} - -// Base SQL query string to fetch all comments. -// Can be extended with a WHEN filter clause. -const selectAllCommentsQueryString = `SELECT -c.id, -c.message, -cu.first_name author_first_name, -cu.last_name author_last_name, -c.time_created, -c.status, -q.message quote_message, -qu.first_name quote_author_first_name, -qu.last_name quote_author_last_name, -q.time_created quote_time_created -FROM comment c -LEFT JOIN "user" cu -ON cu.id = c.author_id -LEFT JOIN comment q -ON q.id = c.quoted_comment_id -LEFT JOIN "user" qu -ON qu.id = q.author_id` - -// A list of messages of varying length for DB seeding. -const mockMessageVariations = [ - 'Lorem ipsum dolor sit amet.', - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem obcaecati ut reprehenderit explicabo, adipisci atque! Repudiandae eos facilis veniam modi.', - 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab dicta error architecto id soluta laborum pariatur saepe doloribus voluptatem voluptas totam placeat, inventore rem! Tempore illum deleniti esse nemo. Amet.', - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur tristique at sem eu ultricies. Curabitur cursus efficitur ipsum, et iaculis ipsum egestas vel egestas vestibulum nec odio posuere, mollis diam et, bibendum velit. Proin non velit nec dui luctus dolor.', - 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Eveniet deleniti ad quasi, ea, recusandae esse autem expedita tempora molestiae ipsa labore magnam dolorem nostrum, corrupti sint obcaecati. Voluptatum molestiae, qui laudantium voluptatibus eius, ratione voluptate, eaque quae alias dicta pariatur?' -] - -// A list of users for DB seeding and random asigning to new -// comments until authentication and session mechanism are in place. -const mockUsers = [ - { firstName: 'Primož', lastName: 'Roglič' }, - { firstName: 'Tadej', lastName: 'Pogačar' }, - { firstName: 'Krištof', lastName: 'Kolumb' }, - { firstName: 'Rudolf', lastName: 'Maister' }, - { firstName: 'Ricky', lastName: 'Rickardo' }, - { firstName: 'Freddy', lastName: 'Mercury' }, - { firstName: 'Roger', lastName: 'Moore' }, - { firstName: 'Michael', lastName: 'Jackson' }, - { firstName: 'John', lastName: 'Elton' }, - { firstName: 'Harry', lastName: 'Potter' } -] - -function pickRandomMockMessage() { - return mockMessageVariations[ - Math.floor(Math.random() * mockMessageVariations.length) - ] -} - -function pickRandomMockUserId() { - return Math.ceil(Math.random() * mockUsers.length) -} - -async function seedMockUsersInDb() { - const { rows } = await db.query('SELECT COUNT(*) user_count FROM "user"') - const userCount = rows[0].user_count - if (+userCount) return 'Users already exist' - - await Promise.all( - mockUsers.map(async user => { - const text = - 'INSERT INTO "user"(username, first_name, last_name, email, bcrypt_hash) VALUES ($1, $2, $3, $4, \'dummyBcryptHash\') RETURNING *' - const values = [ - `${user.firstName}_${user.lastName}`, - user.firstName, - user.lastName, - `${user.firstName}.${user.lastName}@rsdo.com` - ] - const { rows } = await db.query(text, values) - const createdUser = rows[0] - debug(`Created user: ${JSON.stringify(createdUser)}`) - }) - ) - return 'Successfully seeded all users' -} - -async function seedPortalAdmin() { - const { - rows: [{ exists }] - } = await db.query( - "SELECT EXISTS (SELECT 1 FROM user_role WHERE role_name = 'portal admin')" - ) - - if (exists) return 'Skipping creation of portal admin (already exists)' - - const MOCK_ADMIN_BASE = 'admin' - const adminUser = { - username: MOCK_ADMIN_BASE, - firstName: MOCK_ADMIN_BASE, - lastName: MOCK_ADMIN_BASE, - password: portalAdminInitialPassword, - email: portalAdminInitialEmail - } - const userId = await User.create(adminUser) - const assignAdminRole = db.query( - `INSERT INTO user_role (user_id, role_name) - VALUES - ($1, 'portal admin'), - ($1, 'dictionaries admin'), - ($1, 'consultancy admin'), - ($1, 'consultant')`, - [userId] - ) - const activateAdminUser = db.query( - `UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`, - [adminUser.username] - ) - await Promise.all([assignAdminRole, activateAdminUser]) - return `Successfully created portal admin (username: ${adminUser.username}, password: ${adminUser.password})` -} - -async function seedConsultants() { - const { - rows: [mockConsultancyAdmin] - } = await db.query('SELECT id FROM "user" WHERE username = $1', ['cadmin']) - - if (mockConsultancyAdmin) { - return "Consultants already exist: 'cadmin', 'consultant1', 'consultant2', 'consultant3'" - } - - const cadminUser = { - username: 'cadmin', - firstName: 'cadmin', - lastName: 'cadmin', - password: 'cadmin', - email: 'cadmin@rsdo.com' - } - const userId = await User.create(cadminUser) - const assignConsultancyAdminRole = db.query( - `INSERT INTO user_role (user_id, role_name) - VALUES - ($1, 'consultancy admin')`, - [userId] - ) - const activateConsultancyAdminUser = db.query( - `UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`, - [cadminUser.username] - ) - await Promise.all([assignConsultancyAdminRole, activateConsultancyAdminUser]) - - for (let i = 1; i <= 3; i++) { - const consultant = { - username: `consultant${i}`, - firstName: `consultant${i}`, - lastName: `consultant${i}`, - password: `consultant${i}`, - email: `consultant${i}@rsdo.com` - } - const userId = await User.create(consultant) - const assignConsultantRole = db.query( - `INSERT INTO user_role (user_id, role_name) - VALUES - ($1, 'consultant')`, - [userId] - ) - const activateConsultant = db.query( - `UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`, - [consultant.username] - ) - await Promise.all([assignConsultantRole, activateConsultant]) - } - - return `Successfully seeded consultants 'cadmin', 'consultant1', 'consultant2', 'consultant3'` -} - -Comment.seedDummyData = () => { - // Seed DB with mock users on empty DB. - // seedMockUsersInDb() - // .then(debug) - // .catch(err => { - // debug('Users not seeded.') - // debug(err) - // }) - - // Seed DB with mock portal admin user on empty DB. - // TODO Replace with a more robust solution for production. - seedPortalAdmin() - .then(debug) - .catch(err => { - debug('Portal admin not seeded.') - debug(err) - }) - - // Seed DB with mock consultancy admin and consultant users on empty DB. - // seedConsultants() - // .then(debug) - // .catch(err => { - // debug('Consultants not seeded.') - // debug(err) - // }) } module.exports = Comment diff --git a/express/models/consultancy-entry.js b/express/models/consultancy-entry.js index 8e44506..3d03e4d 100644 --- a/express/models/consultancy-entry.js +++ b/express/models/consultancy-entry.js @@ -55,19 +55,6 @@ class ConsultancyEntry { this.formattedTimePublished = formattedTimePublished } - // Fetch all consultancy entries from DB. - static async fetchAll() { - // TODO Luka: Miha, define specific fields instead of using *. - const { rows: fetchedConsEntries } = await db.query(` - SELECT * - FROM consultancy_entry - ORDER BY time_created DESC`) - const deserializedConsEntries = fetchedConsEntries.map( - consEntry => new this(consEntry) - ) - return deserializedConsEntries - } - // Fetch consultancy entry by ID // to_char(time_created,'HH24:MI:SS DD/MM/YYYY') // TODO i18n date format @@ -162,24 +149,6 @@ class ConsultancyEntry { return emails } - // Fetch all consultancy entries filtered by status from DB. - static async fetchAllByStatus(status) { - // TODO Luka: Miha, define specific fields instead of using *. - const sqlQuery = ` - SELECT *, to_char(time_created, 'FMDD. FMMM. YYYY') formatted_time_created - FROM consultancy_entry - WHERE status=$1 - ORDER BY time_created DESC` - - const values = [status] - const { rows: fetchedConsEntries } = await db.query(sqlQuery, values) - - const deserializedConsEntries = fetchedConsEntries.map( - consEntry => new this(consEntry) - ) - return deserializedConsEntries - } - // Fetch all consultancy entries filtered by status from DB. static async fetchAllByStatusCount(status) { // TODO Luka: Miha, define specific fields instead of using *. @@ -311,12 +280,6 @@ class ConsultancyEntry { return deserializedConsEntries } - // Fetch all new consultancy entries from DB. - static async fetchAllNew() { - const newEntries = await this.fetchAllByStatus('new') - return newEntries - } - static async fetchAllRejected() { const newEntries = await this.fetchAllByStatusWithAuthorData('rejected') return newEntries @@ -438,20 +401,6 @@ class ConsultancyEntry { return rows[0] } - static async getEditors(entryId) { - const sqlQuery = ` - SELECT u.id, u.first_name, u.last_name - FROM "consultancy_entry" ce - INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id - INNER JOIN "user" u ON u.id = cec.user_id - WHERE ce.id=$1` - - const values = [entryId] - - const { rows } = await db.query(sqlQuery, values) - return rows - } - static async getSharedAuthorsArray(entryId) { const sqlQuery = `SELECT answer_authors FROM "consultancy_entry" WHERE id=$1;` diff --git a/express/models/demo-paginacija.js b/express/models/demo-paginacija.js deleted file mode 100644 index 3db8a72..0000000 --- a/express/models/demo-paginacija.js +++ /dev/null @@ -1,49 +0,0 @@ -const db = require('./db') - -const DemoPaginacija = {} - -// Metoda za generacijo demo podatkov. -DemoPaginacija.initDemoData = async () => { - await db.query(` - CREATE TABLE IF NOT EXISTS demo_paginacija (zanimivo TEXT, nezanimivo1 TEXT, nezanimivo2 TEXT); - - DO $$ - BEGIN - IF (SELECT COUNT(*) FROM demo_paginacija) = 0 THEN - FOR stevec IN 1..9993 LOOP - INSERT INTO demo_paginacija VALUES ('vrednost' || stevec, 'brezveze', 'tega res ne rabimo'); - END LOOP; - END IF; - END $$`) -} - -// Metoda za poizvedbo demo podatkov za določeno stran. -DemoPaginacija.fetch = async (resultsPerPage, page) => { - const { - rows: [{ result }] - } = await db.query( - ` - SELECT jsonb_build_object( - 'pages_total', ( - SELECT CEIL(COUNT(*) / $1::float) - FROM demo_paginacija - ), - 'results', ARRAY( - SELECT jsonb_build_object( - 'zanimivo', zanimivo, - 'nezanimivo1', nezanimivo1, - 'nezanimivo2', nezanimivo2 - ) - FROM demo_paginacija - LIMIT $1 - OFFSET $2 - ) - ) result - `, - [resultsPerPage, resultsPerPage * (page - 1)] - ) - - return result -} - -module.exports = DemoPaginacija diff --git a/express/models/dictionary.js b/express/models/dictionary.js index 77c7daa..574d0ab 100644 --- a/express/models/dictionary.js +++ b/express/models/dictionary.js @@ -17,14 +17,18 @@ class Dictionary { // TODO Add (language sensitive) dictionary status text conversion. constructor({ id, + name, name_sl: nameSl, + name_en: nameEn, time_modified: timeModified, status, count_entries: countEntries, count_comments: countComments }) { this.id = id + this.name = name this.nameSl = nameSl + this.nameEn = nameEn this.timeModified = timeModified this.status = status this.countEntries = countEntries @@ -32,13 +36,12 @@ class Dictionary { } // Fetch all dictionaries from DB. - // TODO i18n name_sl - static async fetchAll() { + static async fetchAll(determinedLanguage) { // TODO Implement SQL stored procedures or functions. const { rows: fetchedDictionaries } = await db.query(` SELECT id, - name_sl, + name_${determinedLanguage} name, time_modified, status, count_entries, @@ -51,36 +54,13 @@ class Dictionary { return deserializedDictionaries } - /* - // Fetch all dictionaries from DB by it's ID. - static async fetchAllByUser(id) { - // TODO Implement SQL stored procedures or functions. - const text = ` - SELECT - id, - name_sl, - time_modified, - status, - count_entries, - count_comments - FROM dictionary - WHERE id=$1 - ` - const values = [id] - const { rows: fetchedDictionaries } = await db.query(text, values) - const deserializedDictionary = new this(fetchedDictionaries[0]) - return deserializedDictionary - } - */ - - // TODO i18n name_sl // Fetch all dictionaries from DB for which the user has at least one dictionary role. - static async fetchAllByUser(userId) { + static async fetchAllByUser(userId, determinedLanguage) { // TODO Implement SQL stored procedures or functions. const text = ` SELECT id, - name_sl, + name_${determinedLanguage} name, time_modified, status, count_entries, @@ -107,16 +87,14 @@ class Dictionary { } // Fetch all primary domains from DB. - static async fetchAllPrimaryDomains() { + static async fetchAllPrimaryDomains(determinedLanguage) { // TODO Implement SQL stored procedures or functions. const { rows: fetchedDomains } = await db.query(` - SELECT id, name_sl, name_en + SELECT id, name_${determinedLanguage} name FROM domain_primary - ORDER BY id`) - const deserializedDomains = fetchedDomains.map(domain => - deserialize.primaryDomain(domain) - ) - return deserializedDomains + ORDER BY name_${determinedLanguage}`) + + return fetchedDomains } // Fetch primary domain for a specific dictionary from DB. @@ -152,18 +130,16 @@ class Dictionary { } // Fetch all languages from DB. - static async fetchAllLanguages(lang, excludeSlovene) { + static async fetchAllLanguages(determinedLanguage, excludeSlovene) { // TODO Implement SQL stored procedures or functions. const { rows: fetchedLanguages } = await db.query(` SELECT id, - ${lang} + name_${determinedLanguage} name FROM language${excludeSlovene ? "\nWHERE code <> 'sl'" : ''} - ORDER BY ${lang}`) - const deserializedLanguages = fetchedLanguages.map(language => - deserialize.language(language) - ) - return deserializedLanguages + ORDER BY name_${determinedLanguage}`) + + return fetchedLanguages } // Create new dictionary in DB and assign the creator as its admin. @@ -304,11 +280,11 @@ class Dictionary { } // Fetch single dictionary data for editing users from DB. - static async fetchEditUsers(dictionaryId) { + static async fetchEditUsers(dictionaryId, determinedLanguage) { const text = ` SELECT id, - name_sl, + name_${determinedLanguage} name, entries_have_terminology_review_flag, entries_have_language_review_flag, status @@ -385,6 +361,7 @@ class Dictionary { SELECT id, name_sl, + name_en, entries_have_domain_labels, entries_have_label, entries_have_definition, @@ -412,21 +389,19 @@ class Dictionary { } // Fetch single dictionary data for viewing details about it - static async fetchDictionaryBasicInfo(dictionaryId) { - const isInEnglish = false - const lang = isInEnglish ? 'd.name_en' : 'd.name_sl' + static async fetchDictionaryBasicInfo(dictionaryId, determinedLanguage) { const text = ` SELECT d.id, - ${lang} dictionarysl, + d.name_${determinedLanguage} dictionarysl, d.count_entries, to_char(d.time_modified,'YYYY-MM-DD') time_modified, d.issn, d.author, - dp.name_sl domain_primary, + dp.name_${determinedLanguage} domain_primary, description, - l.name_sl languageSl, - ds.name_sl domainSecondarySl, + l.name_${determinedLanguage} languageSl, + ds.name_${determinedLanguage} domainSecondarySl, lp.name portalnamesl, lp.code portalcode FROM @@ -551,7 +526,8 @@ class Dictionary { resultsPerPage, page, orderType, - orderIndex + orderIndex, + determinedLanguage ) { let queryAppend = '' if (domainQuery.length > 0) { @@ -580,22 +556,21 @@ class Dictionary { } } - const isInEnglish = false - const lang = isInEnglish ? 'name_en' : 'name_sl' - - const orderBy = `${orderType}.${lang} ${orderIndex ? 'DESC' : 'ASC'}` + const orderBy = `${orderType}.name_${determinedLanguage} ${ + orderIndex ? 'DESC' : 'ASC' + }` // TODO domain primary language toggle! const text = ` SELECT distinct (d.id), - d.${lang} dictionarysl, + d.name_${determinedLanguage} dictionarysl, d.count_entries, d.count_comments, to_char(d.time_modified,'YYYY-MM-DD') time_modified, d.issn, d.author, - dp.name_sl domain_primary, + dp.name_${determinedLanguage} domain_primary, description, lp.name portalnamesl, lp.code portalcode @@ -605,7 +580,7 @@ class Dictionary { INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id - WHERE d.status = 'published' AND LOWER(d.name_sl) LIKE '%' || LOWER($1) || '%' ${queryAppend} + WHERE d.status = 'published' AND LOWER(d.name_${determinedLanguage}) LIKE '%' || LOWER($1) || '%' ${queryAppend} ORDER BY ${orderBy} LIMIT $2 OFFSET $3` @@ -737,13 +712,12 @@ class Dictionary { } // Fetch languages associated with a single dictionary from DB. - static async fetchLanguages(dictionaryId) { + static async fetchLanguages(dictionaryId, determinedLanguage) { const text = ` SELECT l.id, l.code, - l.name_sl, - l.name_en + l.name_${determinedLanguage} name FROM dictionary d INNER JOIN dictionary_language dl ON d.id = dl.dictionary_id INNER JOIN language l ON dl.language_id = l.id @@ -753,20 +727,16 @@ class Dictionary { const { rows: fetchedLanguages } = await db.query(text, value) - const deserializedLanguages = fetchedLanguages.map(language => - deserialize.language(language) - ) - return deserializedLanguages + return fetchedLanguages } // Fetch latest 3 dictionaries by publish date - static async fetchLatest3DictsByPublishDate(isInEnglish) { - const lang = isInEnglish ? 'd.name_en' : 'd.name_sl' + static async fetchLatest3DictsByPublishDate(determinedLanguage) { const text = ` SELECT d.id, - ${lang} dictionarysl, - dp.name_sl domain_primary, + d.name_${determinedLanguage} dictionarysl, + dp.name_${determinedLanguage} domain_primary, d.count_comments FROM dictionary d INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id @@ -842,7 +812,11 @@ class Dictionary { await db.query(text, values) } - static async fetchAllAdminDictionaries(resultsPerPage, page) { + static async fetchAllAdminDictionaries( + determinedLanguage, + resultsPerPage, + page + ) { // TODO Implement SQL stored procedures or functions. const { rows: [{ result }] @@ -856,7 +830,7 @@ class Dictionary { 'results', ARRAY( SELECT jsonb_build_object( 'id', id, - 'name', name_sl, + 'name', name_${determinedLanguage}, 'timeCreated', time_created, 'timeModified', time_modified, 'status', status @@ -1162,12 +1136,12 @@ class Dictionary { } // Fetch single dictionary's name from DB. - static async fetchName(dictionaryId) { + static async fetchName(dictionaryId, determinedLanguage) { const { rows } = await db.query( - 'SELECT name_sl FROM dictionary WHERE id = $1', + `SELECT name_${determinedLanguage} name FROM dictionary WHERE id = $1`, [dictionaryId] ) - const dictionaryName = rows[0].name_sl + const dictionaryName = rows[0].name return dictionaryName } diff --git a/express/models/entry.js b/express/models/entry.js index c20a283..fd8099c 100644 --- a/express/models/entry.js +++ b/express/models/entry.js @@ -1,7 +1,10 @@ const db = require('./db') const { searchEngineClient, ENTRY_INDEX } = require('./search-engine') const { intoDbArray, getInstanceSetting, removeHtmlTags } = require('./helpers') -const { prepareEntryForIndexing } = require('./helpers/dictionary') +const { + prepareEntryForIndexing, + sanitizeField +} = require('./helpers/dictionary') const Entry = {} @@ -10,7 +13,7 @@ Entry.create = async (userId, dictionaryId, entry) => { const pickedLinks = intoDbArray(entry.links, 'always') const pickedType = intoDbArray(entry.type, 'always') const links = pickedLinks.map((link, index) => ({ - link, + link: sanitizeField.toMixedBasic(link), type: pickedType[index] })) const foreign = intoDbArray(entry.foreign, 'always') @@ -18,9 +21,13 @@ Entry.create = async (userId, dictionaryId, entry) => { if (row.term || row.definition || row.synonym) { agg.push({ language: row.code, - terms: intoDbArray(row.term, 'undefined'), - definition: row.definition || null, - synonyms: intoDbArray(row.synonym, 'undefined') + terms: intoDbArray(row.term, 'undefined')?.map(term => + sanitizeField.toMixedBasic(term) + ), + definition: sanitizeField.toMixedExtended(row.definition) || null, + synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym => + sanitizeField.toMixedBasic(synonym) + ) }) } return agg @@ -34,22 +41,26 @@ Entry.create = async (userId, dictionaryId, entry) => { dictionaryId, isValid, entry.status, - entry.term || null, + sanitizeField.toMixedBasic(entry.term) || null, userId, entry.homonymSort || null, entry.wordforms || null, entry.accent || null, - entry.pronunciation, - intoDbArray(entry.domainLabels, 'always'), - entry.label || null, - entry.definition || null, - intoDbArray(entry.synonyms), + entry.pronunciation || null, + intoDbArray(entry.domainLabels, 'always').map(label => + sanitizeField.toText(label) + ), + sanitizeField.toMixedExtended(entry.label) || null, + sanitizeField.toMixedExtended(entry.definition) || null, + intoDbArray(entry.synonyms)?.map(synonym => + sanitizeField.toMixedBasic(synonym) + ), links, - entry.other || null, + sanitizeField.toMixedOther(entry.other) || null, foreignLanguageContent, - intoDbArray(entry.image), - intoDbArray(entry.audio), - intoDbArray(entry.video) + intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)), + intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)), + intoDbArray(entry.video)?.map(video => sanitizeField.toText(video)) ] const text = `SELECT entry_new (${db.genParamStr(values)})` @@ -60,71 +71,6 @@ Entry.create = async (userId, dictionaryId, entry) => { return entryId } -// // Fetch all entry terms of a single dictionary from DB. -// Entry.fetchAll = async dictionaryId => { -// const text = ` -// SELECT -// e.id, -// e.is_valid as valid, -// e.is_published as published, -// e.term as term, -// MAX(ef.term) as fterm, -// CASE -// WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 week' THEN 'T' -// WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 month' THEN 'M' -// WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 year' THEN 'L' -// ELSE '' -// END comment_age -// FROM entry e -// LEFT JOIN entry_foreign ef ON e.id = ef.entry_id -// WHERE dictionary_id = $1 -// GROUP BY id, is_valid, is_published, e.term, comment_age -// ORDER BY e.term` -// const value = [dictionaryId] - -// const { rows: fetchedTerms } = await db.query(text, value) -// return fetchedTerms -// } - -// Metoda za poizvedbo demo podatkov za določeno stran. -// Entry.fetchPaginated = async (resultsPerPage, page) => { -// const { -// rows: [{ result }] -// } = await db.query( -// ` -// SELECT jsonb_build_object( -// 'pages_total', ( -// SELECT CEIL(COUNT(*) / $1::float) -// FROM demo_paginacija -// ), -// 'results', ARRAY( -// SELECT jsonb_build_object( -// dictionary_id, -// term, -// is_published, -// is_terminology_reviewed, -// is_language_reviewed, -// status, -// label, -// definition, -// synonym, -// other, -// image, -// audio, -// video -// ) -// FROM entry -// LIMIT $1 -// OFFSET $2 -// ) -// ) result -// `, -// [resultsPerPage, resultsPerPage * (page - 1)] -// ) - -// return result -// } - // Fetch all data, related to single entry from DB. Entry.fetchFull = async entryId => { const text = ` @@ -565,7 +511,7 @@ Entry.update = async (userId, entry) => { const pickedLinks = intoDbArray(entry.links, 'always') const pickedType = intoDbArray(entry.type, 'always') const links = pickedLinks.map((link, index) => ({ - link, + link: sanitizeField.toMixedBasic(link), type: pickedType[index] })) const foreign = intoDbArray(entry.foreign, 'always') @@ -573,9 +519,13 @@ Entry.update = async (userId, entry) => { if (row.term || row.definition || row.synonym) { agg.push({ language: row.code, - terms: intoDbArray(row.term, 'undefined'), - definition: row.definition || null, - synonyms: intoDbArray(row.synonym, 'undefined') + terms: intoDbArray(row.term, 'undefined')?.map(term => + sanitizeField.toMixedBasic(term) + ), + definition: sanitizeField.toMixedExtended(row.definition) || null, + synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym => + sanitizeField.toMixedBasic(synonym) + ) }) } return agg @@ -591,19 +541,23 @@ Entry.update = async (userId, entry) => { !!entry.isTerminologyReviewed, !!entry.isLanguageReviewed, entry.status, - entry.term || null, + sanitizeField.toMixedBasic(entry.term) || null, userId, entry.homonymSort || null, - intoDbArray(entry.domainLabels, 'always'), - entry.label || null, - entry.definition || null, - intoDbArray(entry.synonyms), + intoDbArray(entry.domainLabels, 'always').map(label => + sanitizeField.toText(label) + ), + sanitizeField.toMixedExtended(entry.label) || null, + sanitizeField.toMixedExtended(entry.definition) || null, + intoDbArray(entry.synonyms)?.map(synonym => + sanitizeField.toMixedBasic(synonym) + ), links, - entry.other || null, + sanitizeField.toMixedOther(entry.other) || null, foreignLanguageContent, - intoDbArray(entry.image), - intoDbArray(entry.audio), - intoDbArray(entry.video) + intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)), + intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)), + intoDbArray(entry.video)?.map(video => sanitizeField.toText(video)) ] const text = `SELECT entry_update (${db.genParamStr(values)})` diff --git a/express/models/extraction.js b/express/models/extraction.js index 6954716..6dc2642 100644 --- a/express/models/extraction.js +++ b/express/models/extraction.js @@ -68,13 +68,22 @@ Extraction.fetch = async id => { const { rows: [fetchedExtraction] } = await db.query( - 'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE id = $1', + 'SELECT id, user_id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE id = $1', [id] ) return deserialize.extraction(fetchedExtraction) } +// Fetch oss document types from DB. +Extraction.fetchOssDocumentTypes = async determinedLanguage => { + const { rows: fetchedDocumentTypes } = await db.query( + `SELECT id, name_${determinedLanguage} name FROM extraction_oss_document_types` + ) + + return fetchedDocumentTypes +} + // Fetch data of the author of a specific extraction entry from DB. Extraction.fetchAuthorData = async id => { const { @@ -249,21 +258,29 @@ Extraction.processOwn = async function (extractionId, extractionName) { const conllusPath = getConllusPath(extractionId) const conllusPaths = [] const MAX_BODY_LENGTH = 10 ** 9 // 1 GB + const RETRY_SECONDS_INTERVAL = 60 // 1 minute + const RETRY_SECONDS_MAX = 60 * 60 * 24 // 1 day // Using remote API, transform each document into conllu format. for (const documentName of documentNames) { const filePath = `${documentsPath}/${documentName}` - const form = new FormData() - form.append('file', createReadStream(filePath), documentName) try { - const { data: data1 } = await axios.post( - `${extractionApiOrigin}/datotekaVConlluAsync`, - form, - { - headers: { - ...form.getHeaders() - }, - maxBodyLength: MAX_BODY_LENGTH - } + const { data: data1 } = await retry( + async () => { + const form = new FormData() + form.append('file', createReadStream(filePath), documentName) + return await axios.post( + `${extractionApiOrigin}/datotekaVConlluAsync`, + form, + { + headers: { + ...form.getHeaders() + }, + maxBodyLength: MAX_BODY_LENGTH + } + ) + }, + RETRY_SECONDS_INTERVAL, + RETRY_SECONDS_MAX ) const remotejobId = +data1.check_job_url.split('/').at(-1) await db.query( @@ -276,9 +293,14 @@ Extraction.processOwn = async function (extractionId, extractionName) { // Poll job until finished. while (true) { await sleep(5) - const { data: data2 } = await axios.get( - `${extractionApiOrigin}/job/${remotejobId}` + const { data: data2 } = await retry( + async () => { + return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`) + }, + RETRY_SECONDS_INTERVAL, + RETRY_SECONDS_MAX ) + if (data2.finished_on) { if (data2.job_status !== 'finished processing (OK)') { throw Error( @@ -326,18 +348,25 @@ Extraction.processOwn = async function (extractionId, extractionName) { stopTerms.forEach(stopTerm => stopTermsSet.add(stopTerm.trim())) } stopTermsSet.delete('') + const stopTermsArr = Array.from(stopTermsSet) const termCandidatesPath = getTermCandidatesPath(extractionId) try { - const { data: data3 } = await axios.post( - `${extractionApiOrigin}/izlusciAsync`, - { - conllus: conllusArr, - prepovedaneBesede: Array.from(stopTermsSet), - // TODO Enabled for all cases. Add a switch for users later. - definicije: true + const { data: data3 } = await retry( + async () => { + return await axios.post( + `${extractionApiOrigin}/izlusciAsync`, + { + conllus: conllusArr, + prepovedaneBesede: stopTermsArr, + // TODO Enabled for all cases. Add a switch for users later. + definicije: true + }, + { maxBodyLength: MAX_BODY_LENGTH } + ) }, - { maxBodyLength: MAX_BODY_LENGTH } + RETRY_SECONDS_INTERVAL, + RETRY_SECONDS_MAX ) const remotejobId = +data3.check_job_url.split('/').at(-1) await db.query( @@ -348,8 +377,12 @@ Extraction.processOwn = async function (extractionId, extractionName) { // Poll job until finished. while (true) { await sleep(5) - const { data: data4 } = await axios.get( - `${extractionApiOrigin}/job/${remotejobId}` + const { data: data4 } = await retry( + async () => { + return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`) + }, + RETRY_SECONDS_INTERVAL, + RETRY_SECONDS_MAX ) if (data4.finished_on) { const { job_result: jobResult } = data4 @@ -395,6 +428,11 @@ Extraction.processOwn = async function (extractionId, extractionName) { title: extractionName }) + await db.query('UPDATE extraction SET corpus_id = $1 WHERE id = $2', [ + corpusId, + extractionId + ]) + // Wait for creation of corpus. while (true) { console.log('SLEEP FOR 5 SECS') @@ -484,8 +522,8 @@ Extraction.processOwn = async function (extractionId, extractionName) { ) await db.query( - "UPDATE extraction SET status = 'finished', time_finished = NOW(), corpus_id = $1 WHERE id = $2", - [corpusId, extractionId] + "UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1", + [extractionId] ) console.log('EXTRACTION SUCCESSFUL') } catch (error) { @@ -503,6 +541,8 @@ Extraction.processOss = async function (extractionId, ossParams) { // TODO Probably not, at least not while the the OSS enpoint is GET, due to limited length of URLs. // TODO Also consider refactoring certain parts, // TODO as some are identical or similar to Own variants or used earlier in the same pipeline. + const RETRY_SECONDS_INTERVAL = 60 // 1 minute + const RETRY_SECONDS_MAX = 60 * 60 * 24 // 1 day const stopTermsPath = getStopTermsPath(extractionId) const stopTermsFilesNames = await this.fetchAllStopTermsFilesNames( extractionId @@ -532,7 +572,13 @@ Extraction.processOss = async function (extractionId, ossParams) { const extractApiUrl = `${extractionApiOrigin}/oss/izlusciPoIskanjuAsync?${searchParams}` try { - const { data: data1 } = await axios.get(extractApiUrl) + const { data: data1 } = await retry( + async () => { + return await axios.get(extractApiUrl) + }, + RETRY_SECONDS_INTERVAL, + RETRY_SECONDS_MAX + ) const remotejobId = +data1.check_job_url.split('/').at(-1) await db.query( "UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4", @@ -542,8 +588,12 @@ Extraction.processOss = async function (extractionId, ossParams) { // Poll job until finished. while (true) { await sleep(5) - const { data: data2 } = await axios.get( - `${extractionApiOrigin}/job/${remotejobId}` + const { data: data2 } = await retry( + async () => { + return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`) + }, + RETRY_SECONDS_INTERVAL, + RETRY_SECONDS_MAX ) if (data2.finished_on) { if ( @@ -618,4 +668,49 @@ function logExtractionError(error, extractionId, jobType, filename) { console.error(error) } +async function retry(callback, everySeconds, maxSeconds) { + const startTime = new Date() + let numOfRetries = 0 + + /* eslint-disable no-console */ + while (true) { + try { + const result = await callback() + if (numOfRetries) { + console.log( + `Recovered after ${numOfRetries} retries and ${Math.floor( + (new Date() - startTime) / 1000 + )} seconds` + ) + } + return result + } catch (error) { + const secondsSinceStart = Math.floor((new Date() - startTime) / 1000) + const nextRetrySeconds = secondsSinceStart + everySeconds + + console.log('Failed inside retry') + console.log( + error.isAxiosError ? `Axios message: ${error.message}` : error + ) + console.log({ + numOfRetries, + secondsSinceStart, + everySeconds, + nextRetrySeconds, + maxSeconds + }) + + if (nextRetrySeconds > maxSeconds) { + console.log('FAILING RETRIES') + throw error + } + + console.log(`RETRYING IN ${everySeconds} SECONDS`) + numOfRetries++ + await sleep(everySeconds) + } + } + /* eslint-enable no-console */ +} + module.exports = Extraction diff --git a/express/models/helpers/dictionary/import-file.js b/express/models/helpers/dictionary/import-file.js index 4635572..0bf1d28 100644 --- a/express/models/helpers/dictionary/import-file.js +++ b/express/models/helpers/dictionary/import-file.js @@ -1,9 +1,9 @@ const fs = require('fs') const xmlFlow = require('xml-flow') -const xss = require('xss') const debug = require('debug')('termPortal:models/helpers/dictionary') const db = require('../../db') const { intoDbArray } = require('..') +const { sanitizeField } = require('./index') const JOBS_MAX = 50 const JOBS_MIN = 15 @@ -199,84 +199,18 @@ function handleEntryXml( } } -const markupFilter = { - noMixed: new xss.FilterXSS({ - whiteList: {}, - stripIgnoreTag: true, - stripIgnoreTagBody: ['script', 'style'] - }), - - mixedBasic: new xss.FilterXSS({ - whiteList: { - sup: [], - sub: [] - }, - stripIgnoreTag: true, - stripIgnoreTagBody: ['script', 'style'] - }), - - mixedExtended: new xss.FilterXSS({ - whiteList: { - sup: [], - sub: [], - b: [], - i: [], - a: ['href'] - }, - stripIgnoreTag: true, - stripIgnoreTagBody: ['script', 'style'], - onTag: customTagHandler - }), - - mixedOther: new xss.FilterXSS({ - whiteList: { - sup: [], - sub: [], - b: [], - i: [], - a: ['href'], - br: [] - }, - stripIgnoreTag: true, - stripIgnoreTagBody: ['script', 'style'], - onTag: customTagHandler - }) -} - -function customTagHandler(tag, html, { isWhite, isClosing }) { - // Special treatment only for whitelisted opening anchor tags. - if (tag !== 'a' || !isWhite || isClosing) return - - const matchUrl = html.match(/href="?(?https?:\/\/.*?)"?[\s>]/) - const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined - - return `` -} - function toText(markupObj) { - return markupFilter.noMixed - .process(xmlFlow.toXml(markupObj)) - .replace(/\s+/g, ' ') - .trim() + return sanitizeField.toText(xmlFlow.toXml(markupObj)) } function toMixedBasic(markupObj) { - return markupFilter.mixedBasic - .process(xmlFlow.toXml(markupObj)) - .replace(/\s+/g, ' ') - .trim() + return sanitizeField.toMixedBasic(xmlFlow.toXml(markupObj)) } function toMixedExtended(markupObj) { - return markupFilter.mixedExtended - .process(xmlFlow.toXml(markupObj)) - .replace(/\s+/g, ' ') - .trim() + return sanitizeField.toMixedExtended(xmlFlow.toXml(markupObj)) } function toMixedOther(markupObj) { - return markupFilter.mixedOther - .process(xmlFlow.toXml(markupObj)) - .replace(/\s+/g, ' ') - .trim() + return sanitizeField.toMixedOther(xmlFlow.toXml(markupObj)) } diff --git a/express/models/helpers/dictionary/index.js b/express/models/helpers/dictionary/index.js index 1526d41..8c9ca61 100644 --- a/express/models/helpers/dictionary/index.js +++ b/express/models/helpers/dictionary/index.js @@ -1,3 +1,4 @@ +const xss = require('xss') const { removeHtmlTags } = require('../../helpers') const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine') const { DATA_FILES_PATH } = require('../../../config/settings') @@ -6,7 +7,7 @@ exports.deserialize = { dictionary(dictionary) { const deserializedDictionary = { id: dictionary.id, - nameSl: dictionary.name_sl, + name: dictionary.name, timeModified: dictionary.time_modified, status: dictionary.status, countEntries: dictionary.count_entries, @@ -48,16 +49,16 @@ exports.deserialize = { return deserializedDomain }, - language(language) { - const deserializedLanguage = { - id: language.id, - code: language.code, - nameSl: language.name_sl, - nameEn: language.name_en - } + // language(language) { + // const deserializedLanguage = { + // id: language.id, + // code: language.code, + // nameSl: language.name_sl, + // nameEn: language.name_en + // } - return deserializedLanguage - }, + // return deserializedLanguage + // }, editDescription(dictionary) { const deserializedDictionary = { @@ -77,7 +78,7 @@ exports.deserialize = { editUsers(dictionary) { const deserializedDictionary = { id: dictionary.id, - nameSl: dictionary.name_sl, + name: dictionary.name, terminologyReviewFlag: dictionary.entries_have_terminology_review_flag, languageReviewFlag: dictionary.entries_have_language_review_flag, status: dictionary.status @@ -90,6 +91,7 @@ exports.deserialize = { const deserializedDictionary = { id: dictionary.id, nameSl: dictionary.name_sl, + nameEn: dictionary.name_en, hasDomainLabels: dictionary.entries_have_domain_labels, hasLabel: dictionary.entries_have_label, hasDefinition: dictionary.entries_have_definition, @@ -275,3 +277,79 @@ exports.prepareEntryForIndexing = prepareEntryForIndexing exports.getExportFilesPath = dictId => { return `${DATA_FILES_PATH}/dict_export/${dictId}` } + +const markupFilter = { + noMixed: new xss.FilterXSS({ + whiteList: {}, + stripIgnoreTag: true, + stripIgnoreTagBody: ['script', 'style'] + }), + + mixedBasic: new xss.FilterXSS({ + whiteList: { + sup: [], + sub: [] + }, + stripIgnoreTag: true, + stripIgnoreTagBody: ['script', 'style'] + }), + + mixedExtended: new xss.FilterXSS({ + whiteList: { + sup: [], + sub: [], + b: [], + i: [], + a: ['href'] + }, + stripIgnoreTag: true, + stripIgnoreTagBody: ['script', 'style'], + onTag: customTagHandler + }), + + mixedOther: new xss.FilterXSS({ + whiteList: { + sup: [], + sub: [], + b: [], + i: [], + a: ['href'], + br: [] + }, + stripIgnoreTag: true, + stripIgnoreTagBody: ['script', 'style'], + onTag: customTagHandler + }) +} + +function customTagHandler(tag, html, { isWhite, isClosing }) { + // Special treatment only for whitelisted opening anchor tags. + if (tag !== 'a' || !isWhite || isClosing) return + + const matchUrl = html.match(/href="?(?https?:\/\/.*?)"?[\s>]/) + const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined + + return `` +} + +function sanitize(string, filter) { + return filter.process(string).replace(/\s+/g, ' ').trim() +} + +exports.sanitizeField = { + toText(string) { + return sanitize(string, markupFilter.noMixed) + }, + + toMixedBasic(string) { + return sanitize(string, markupFilter.mixedBasic) + }, + + toMixedExtended(string) { + return sanitize(string, markupFilter.mixedExtended) + }, + + toMixedOther(string) { + return sanitize(string, markupFilter.mixedOther) + } +} diff --git a/express/models/helpers/extraction.js b/express/models/helpers/extraction.js index 90c6574..896dbe0 100644 --- a/express/models/helpers/extraction.js +++ b/express/models/helpers/extraction.js @@ -9,6 +9,7 @@ exports.deserialize = { extraction(extraction) { const deserializedExtraction = { id: extraction.id, + userId: extraction.user_id, name: extraction.name, status: extraction.status, corpusId: extraction.corpus_id, diff --git a/express/models/helpers/search/index.js b/express/models/helpers/search/index.js index 1ec7bc6..4477b05 100644 --- a/express/models/helpers/search/index.js +++ b/express/models/helpers/search/index.js @@ -50,7 +50,7 @@ exports.prepareEntries = hits => { } // Transform search engine's aggregation raw output into correct and friendly format. -exports.prepareAggregation = async aggregationRaw => { +exports.prepareAggregation = async (aggregationRaw, determinedLanguage) => { const { aggregations, hits } = aggregationRaw.body const hitsCount = hits.total.value @@ -107,7 +107,8 @@ exports.prepareAggregation = async aggregationRaw => { const names = await Portal.getSearchAggregateNames( primaryDomainIds, dictionaryIds, - languageIds + languageIds, + determinedLanguage ) const aggregation = { diff --git a/express/models/helpers/user/index.js b/express/models/helpers/user/index.js index 49d6825..885365d 100644 --- a/express/models/helpers/user/index.js +++ b/express/models/helpers/user/index.js @@ -6,6 +6,7 @@ exports.deserialize = { firstName: user.first_name, lastName: user.last_name, email: user.email, + status: user.status, hitsPerPage: user.hits_per_page, language: user.language, userRoles: user.user_roles, diff --git a/express/models/portal.js b/express/models/portal.js index aa91db8..139c262 100644 --- a/express/models/portal.js +++ b/express/models/portal.js @@ -455,27 +455,28 @@ Portal.getSlovenianLanguageId = async () => { Portal.getSearchAggregateNames = async ( primaryDomainIds, dictionaryIds, - languageIds + languageIds, + determinedLanguage ) => { const text = ` SELECT jsonb_build_object( 'primaryDomains', jsonb_object( ARRAY( - SELECT ARRAY [id, name_sl]::TEXT[] + SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[] FROM domain_primary WHERE id = ANY ($1) ) ), 'dictionaries', jsonb_object( ARRAY( - SELECT ARRAY [id, name_sl]::TEXT[] + SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[] FROM dictionary WHERE id = ANY ($2) ) ), 'languages', jsonb_object( ARRAY( - SELECT ARRAY [id, name_sl]::TEXT[] + SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[] FROM language WHERE id = ANY ($3) ) diff --git a/express/models/user.js b/express/models/user.js index 3615f77..a7c1ec3 100644 --- a/express/models/user.js +++ b/express/models/user.js @@ -1,13 +1,60 @@ +const { randomBytes } = require('crypto') +const { promisify } = require('util') +const RandomBytesAsync = promisify(randomBytes) const db = require('./db') const bcrypt = require('bcrypt') const uid = require('uid-safe') const { deserialize } = require('./helpers/user') +const { + ACTIVATION_TOKEN_VALID_DAYS, + CHANGE_EMAIL_TOKEN_VALID_DAYS +} = require('../config/settings') + +const SALT_ROUNDS = 12 +const PASSWORD_RESET_VALID_INTERVAL = '1 day' const User = {} +// Check if a user with provided email already exists. +User.isEmailAlreadyTaken = async email => { + const { rows } = await db.query('SELECT 1 FROM "user" WHERE email = $1', [ + email + ]) + + const isTaken = rows.length > 0 + + return isTaken +} + // Create new user in DB. -User.create = async user => { - const SALT_ROUNDS = 12 +User.create = async (user, t) => { + const { + rows: [userWithSameEmail] + } = await db.query('SELECT status FROM "user" WHERE email = $1', [user.email]) + + if (userWithSameEmail && userWithSameEmail.status !== 'registered') { + const err = Error(t('Elektronski naslov uporablja že drug uporabnik.')) + err.status = 400 + err.displayInProd = true + + throw err + } + + const { + rows: [isUsernameAlreadyTakenByOther] + } = await db.query( + 'SELECT 1 FROM "user" WHERE username = $1 AND email <> $2', + [user.username, user.email] + ) + + if (isUsernameAlreadyTakenByOther) { + const err = Error(t('Izbrano uporabniško ime uporablja že drug uporabnik.')) + err.status = 400 + err.displayInProd = true + + throw err + } + const bcryptHash = await bcrypt.hash(user.password, SALT_ROUNDS) const values = [ @@ -19,7 +66,19 @@ User.create = async user => { ] if (user.language) values.push(user.language) - const text = `INSERT INTO "user" ( + let text + + if (userWithSameEmail) { + text = `UPDATE "user" SET + username = $1, + first_name = $2, + last_name = $3, + bcrypt_hash = $5 + ${user.language ? ', language = $6' : ''} + WHERE email = $4 + RETURNING id` + } else { + text = `INSERT INTO "user" ( username, first_name, last_name, @@ -29,6 +88,7 @@ User.create = async user => { ) VALUES (${db.genParamStr(values)}) RETURNING id` + } const { rows } = await db.query(text, values) @@ -45,36 +105,42 @@ User.saveActivationToken = async (userId, activationToken) => { ) } -// Fetch user from DB by (valid) activation token. -User.fetchByActivationToken = async activationToken => { - const TOKEN_VALID_PERIOD = '1 week' - const text = ` - SELECT u.id - FROM user_token_activation t - INNER JOIN "user" u ON u.id = t.user_id - WHERE - t.token = $1 - AND AGE(NOW(), t.time_created) < INTERVAL '${TOKEN_VALID_PERIOD}' - ` - const values = [activationToken] +// Activate user account using the provided activation token. +User.activateAccountWithToken = async (token, t) => { + let user - const { rows } = await db.query(text, values) - const user = rows[0] + await db.transaction(async dbClient => { + const { rows } = await dbClient.query( + `SELECT user_id FROM user_token_activation WHERE token = $1 AND NOW() - time_created < '${ACTIVATION_TOKEN_VALID_DAYS} days'`, + [token] + ) - // TODO Perhaps suggest to the user to request another one and make a shortcut. - if (!user) throw Error('Povezava je neveljavna ali pa je že potekla') + if (rows.length === 0) { + const err = Error( + t('Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.') + ) + err.status = 403 + err.displayInProd = true + + throw err + } + + const userId = rows[0].user_id + ;({ + rows: [user] + } = await dbClient.query( + `UPDATE "user" SET status = 'active', time_activated = NOW() WHERE id = $1 RETURNING id`, + [userId] + )) + + await dbClient.query('DELETE FROM user_token_activation WHERE token = $1', [ + token + ]) + }) return user } -// Activate user account. -User.activateAccount = async user => { - await db.query( - `UPDATE "user" SET status = 'active', time_activated = NOW() WHERE id = $1`, - [user.id] - ) -} - // Generate a user remember me token. User.generateRememberMeToken = async () => { const token = await uid(32) @@ -96,6 +162,130 @@ User.clearRememberMeToken = async rememberMeToken => { ]) } +// Save a password reset token for a single user in DB. +User.saveResetPasswordToken = async (userId, resetPasswordToken) => { + await db.query( + 'INSERT INTO user_token_reset_password (token, user_id) VALUES ($1, $2)', + [resetPasswordToken, userId] + ) +} + +// Check existance and validity of password reset token in DB. +User.isResetPasswordTokenValid = async token => { + const { rows } = await db.query( + `SELECT 1 exists FROM user_token_reset_password WHERE token = $1 AND NOW() - time_created < '${PASSWORD_RESET_VALID_INTERVAL}'`, + [token] + ) + const isValid = rows.length > 0 + + return isValid +} + +// Set new password for user using the provided reset password token. +User.resetPasswordWithToken = async (token, password, t) => { + let user + + await db.transaction(async dbClient => { + const { rows } = await dbClient.query( + `SELECT user_id FROM user_token_reset_password WHERE token = $1 AND NOW() - time_created < '${PASSWORD_RESET_VALID_INTERVAL}'`, + [token] + ) + + if (rows.length === 0) { + const err = Error( + t( + 'Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.' + ) + ) + err.status = 403 + err.displayInProd = true + + throw err + } + + const bcryptHash = await bcrypt.hash(password, SALT_ROUNDS) + const userId = rows[0].user_id + ;({ + rows: [user] + } = await dbClient.query( + 'UPDATE "user" SET bcrypt_hash = $1 WHERE id = $2 RETURNING id, username, email', + [bcryptHash, userId] + )) + + await dbClient.query( + 'DELETE FROM user_token_reset_password WHERE token = $1', + [token] + ) + }) + + return user +} + +// Save change email token for a single user in DB. +User.saveChangeEmailToken = async (userId, changeEmailToken, newEmail) => { + await db.query( + 'INSERT INTO user_token_change_email (token, user_id, new_email) VALUES ($1, $2, $3)', + [changeEmailToken, userId, newEmail] + ) +} + +// Set new email for user using the provided change email token. +User.changeEmailWithToken = async function (token, t) { + let user + + await db.transaction(async dbClient => { + const { rows } = await dbClient.query( + `SELECT user_id, new_email FROM user_token_change_email WHERE token = $1 AND NOW() - time_created < '${CHANGE_EMAIL_TOKEN_VALID_DAYS} days'`, + [token] + ) + + if (rows.length === 0) { + const err = Error( + t('Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.') + ) + err.status = 403 + err.displayInProd = true + + throw err + } + + const { user_id: userId, new_email: newEmail } = rows[0] + if (await this.isEmailAlreadyTaken(newEmail)) { + const err = Error(t('Elektronski naslov uporablja že drug uporabnik.')) + err.status = 403 + err.displayInProd = true + + throw err + } + + ;({ + rows: [user] + } = await dbClient.query( + 'UPDATE "user" SET email = $1 WHERE id = $2 RETURNING id, username, email', + [newEmail, userId] + )) + + await dbClient.query( + 'DELETE FROM user_token_change_email WHERE token = $1', + [token] + ) + }) + + return user +} + +// Fetch user from DB by username or email. +User.fetchByUsernameOrEmail = async usernameOrEmail => { + const { + rows: [user] + } = await db.query( + 'SELECT id, username, email, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1', + [usernameOrEmail] + ) + + return user +} + // Fetch user data that should be available on every request from DB by id. User.fetchDeserializedDataById = async userId => { const text = ` @@ -105,6 +295,7 @@ User.fetchDeserializedDataById = async userId => { u.first_name, u.last_name, u.email, + u.status, u.hits_per_page, u.language, ARRAY( @@ -148,6 +339,7 @@ User.fetchAll = async (resultsPerPage, page) => { 'pages_total', ( SELECT CEIL(COUNT(*) / $1::float) FROM "user" + WHERE status <> 'closed' ), 'results', ARRAY( SELECT jsonb_build_object( @@ -157,6 +349,7 @@ User.fetchAll = async (resultsPerPage, page) => { 'status', status ) FROM "user" + WHERE status <> 'closed' ORDER BY username LIMIT $1 OFFSET $2 @@ -281,8 +474,12 @@ User.updateUser = async (userId, payload) => { const { rows } = await db.query(previousStatusText, [userId]) const previousStatus = rows[0].status + if (previousStatus === 'closed') throw Error() + let statusValue + let setTimeActivated = false if (previousStatus === 'registered') { + setTimeActivated = !!payload.status statusValue = !payload.status ? 'registered' : 'active' } else statusValue = !payload.status ? 'inactive' : 'active' @@ -293,6 +490,7 @@ User.updateUser = async (userId, payload) => { first_name = $3, last_name = $4, status = $5 + ${setTimeActivated ? ', time_activated = NOW()' : ''} WHERE id = $1` const values = [ @@ -448,12 +646,9 @@ User.insertNewConsultantWithDomain = async (userId, domains) => { // Insert new consultant role with domain of User.insertNewConsultantWithDomainByUsername = async (username, domains) => { - const { rows } = await db.query( - 'SELECT id FROM "user" WHERE username = $1 or email = $1', - [username] - ) + const user = await User.fetchByUsernameOrEmail(username) - await User.insertNewConsultantWithDomain(rows[0].id, domains) + await User.insertNewConsultantWithDomain(user.id, domains) } // Remove consultant role @@ -471,13 +666,15 @@ User.fetchAllowedHitsPerPage = async () => { ).rows.map(e => e.unnest) } -User.updateFirstNameAndLastName = async (username, firstName, lastName) => { - return await db.query( - `UPDATE "user" - SET first_name=$2, last_name=$3 - WHERE username=$1;`, - [username, firstName, lastName] +User.updateFirstNameAndLastName = async (userId, firstName, LastName) => { + const { + rows: [{ email }] + } = await db.query( + 'UPDATE "user" SET first_name = $1, last_name = $2 WHERE id = $3 RETURNING email', + [firstName, LastName, userId] ) + + return email } User.updateHitsPerPage = async (username, hitsPerPageAmount) => { @@ -497,4 +694,83 @@ User.updateLanguage = async (userId, languageCode) => { ]) } +// Change user's password. +User.changePassword = async (userId, passwordOld, passwordNew, t) => { + await db.transaction(async dbClient => { + const { + rows: [{ bcrypt_hash: bcryptHashOld }] + } = await dbClient.query('SELECT bcrypt_hash FROM "user" WHERE id = $1', [ + userId + ]) + + const isOldPasswordCorrect = await bcrypt.compare( + passwordOld, + bcryptHashOld + ) + + if (!isOldPasswordCorrect) { + const err = Error(t('Nepravilno staro geslo.')) + err.status = 403 + err.displayInProd = true + + throw err + } + + const bcryptHashNew = await bcrypt.hash(passwordNew, SALT_ROUNDS) + + await dbClient.query('UPDATE "user" SET bcrypt_hash = $1 WHERE id = $2', [ + bcryptHashNew, + userId + ]) + }) +} + +// Close user's account and anonymize any personal data. +User.closeAccount = async userId => { + const maskString = '#####' + const randomString = (await RandomBytesAsync(10)).toString('hex') + + const anonymizedUsername = randomString + const anonymizedFirstName = maskString + const anonymizedLastName = maskString + const anonymizedEmail = randomString + + await db.transaction(async dbClient => { + await Promise.all([ + dbClient.query( + ` + UPDATE "user" + SET + username = $1, + first_name = $2, + last_name = $3, + email = $4, + status = 'closed', + time_closed = NOW() + WHERE id = $5`, + [ + anonymizedUsername, + anonymizedFirstName, + anonymizedLastName, + anonymizedEmail, + userId + ] + ), + dbClient.query('DELETE FROM user_token_activation WHERE user_id = $1', [ + userId + ]), + dbClient.query('DELETE FROM user_token_remember_me WHERE user_id = $1', [ + userId + ]), + dbClient.query( + 'DELETE FROM user_token_reset_password WHERE user_id = $1', + [userId] + ), + dbClient.query('DELETE FROM user_token_change_email WHERE user_id = $1', [ + userId + ]) + ]) + }) +} + module.exports = User diff --git a/express/package-lock.json b/express/package-lock.json index 87f3a87..33120cf 100644 --- a/express/package-lock.json +++ b/express/package-lock.json @@ -12,6 +12,7 @@ "async": "^3.2.3", "axios": "^0.26.1", "bcrypt": "^5.0.1", + "connect-flash-plus": "^0.2.1", "connect-redis": "^6.0.0", "cookie-parser": "^1.4.5", "debug": "^4.3.2", @@ -794,6 +795,14 @@ "node": ">=8" } }, + "node_modules/connect-flash-plus": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/connect-flash-plus/-/connect-flash-plus-0.2.1.tgz", + "integrity": "sha512-MqnJms7FpZFFlLMaooLviOWwc04chmcPKaqwStjFIYd8MthE99f71yTzUcoyx1XIx87VsJuxzraL/ScbfrUUfQ==", + "engines": { + "node": ">= 0.12.0" + } + }, "node_modules/connect-redis": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz", @@ -4418,6 +4427,11 @@ "xdg-basedir": "^4.0.0" } }, + "connect-flash-plus": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/connect-flash-plus/-/connect-flash-plus-0.2.1.tgz", + "integrity": "sha512-MqnJms7FpZFFlLMaooLviOWwc04chmcPKaqwStjFIYd8MthE99f71yTzUcoyx1XIx87VsJuxzraL/ScbfrUUfQ==" + }, "connect-redis": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz", diff --git a/express/package.json b/express/package.json index 5b67ece..b22db6a 100644 --- a/express/package.json +++ b/express/package.json @@ -12,6 +12,7 @@ "async": "^3.2.3", "axios": "^0.26.1", "bcrypt": "^5.0.1", + "connect-flash-plus": "^0.2.1", "connect-redis": "^6.0.0", "cookie-parser": "^1.4.5", "debug": "^4.3.2", diff --git a/express/public/images/alert-circle-big.svg b/express/public/images/alert-circle-big.svg new file mode 100644 index 0000000..950bab8 --- /dev/null +++ b/express/public/images/alert-circle-big.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/express/public/javascripts/admin.js b/express/public/javascripts/admin.js index 608e660..3230d80 100644 --- a/express/public/javascripts/admin.js +++ b/express/public/javascripts/admin.js @@ -1,4 +1,4 @@ -/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, i18next */ +/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, isI18nReady, i18next, validator */ // const currentPagePath = location.pathname @@ -144,62 +144,64 @@ function initAdmin() { } if (currentPagePath === '/admin/uporabniki/seznam') { - const resultsListEl = document.getElementById('page-results') + isI18nReady.then(t => { + const resultsListEl = document.getElementById('page-results') - const updatePager = initPagination('pagination', onPageChange) + const updatePager = initPagination('pagination', onPageChange) - async function onPageChange(newPage) { - try { - const { page, numberOfAllPages, results } = await getDataForPage( - newPage - ) - removeAllChildNodes(resultsListEl) - renderResults(results) - updatePager(page, numberOfAllPages) - } catch (error) { - let message = i18next.t('Prišlo je do napake.') - if (error.response?.data) { - message = error.response.data - } else if (error.request) { - message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.') + async function onPageChange(newPage) { + try { + const { page, numberOfAllPages, results } = await getDataForPage( + newPage + ) + removeAllChildNodes(resultsListEl) + renderResults(results) + updatePager(page, numberOfAllPages) + } catch (error) { + let message = i18next.t('Prišlo je do napake.') + if (error.response?.data) { + message = error.response.data + } else if (error.request) { + message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.') + } + alert(message) + updatePager() } - alert(message) - updatePager() } - } - async function getDataForPage(page) { - const url = `/api/v1/users/listAllUsers?p=${page}` - const { data } = await axios.get(url) - return data - } + async function getDataForPage(page) { + const url = `/api/v1/users/listAllUsers?p=${page}` + const { data } = await axios.get(url) + return data + } - function renderResults(results) { - results.forEach(result => { - const rowEl = document.createElement('tr') - const td1 = document.createElement('td') - const td2 = document.createElement('td') - const td3 = document.createElement('td') - const td4 = document.createElement('td') - const aEl = document.createElement('a') - const imgEl = document.createElement('img') - const spanEl = document.createElement('span') - td1.textContent = result.userName - td2.textContent = result.email - td3.textContent = result.status - aEl.classList.add('image-link') - aEl.type = 'link' - aEl.href = `/admin/uporabniki/${result.id}/urejanje` - imgEl.src = '/images/u_edit-alt.svg' - imgEl.alt = i18next.t('Uredi') - spanEl.className = 'normal-gray ms-1' - spanEl.textContent = i18next.t('Uredi') - td4.append(aEl) - aEl.append(imgEl, spanEl) - rowEl.append(td1, td2, td3, td4) - resultsListEl.appendChild(rowEl) - }) - } + function renderResults(results) { + results.forEach(result => { + const rowEl = document.createElement('tr') + const td1 = document.createElement('td') + const td2 = document.createElement('td') + const td3 = document.createElement('td') + const td4 = document.createElement('td') + const aEl = document.createElement('a') + const imgEl = document.createElement('img') + const spanEl = document.createElement('span') + td1.textContent = result.userName + td2.textContent = result.email + td3.textContent = t(`userStatus${result.status}`) + aEl.classList.add('image-link') + aEl.type = 'link' + aEl.href = `/admin/uporabniki/${result.id}/urejanje` + imgEl.src = '/images/u_edit-alt.svg' + imgEl.alt = i18next.t('Uredi') + spanEl.className = 'normal-gray ms-1' + spanEl.textContent = i18next.t('Uredi') + td4.append(aEl) + aEl.append(imgEl, spanEl) + rowEl.append(td1, td2, td3, td4) + resultsListEl.appendChild(rowEl) + }) + } + }) } if (currentPagePath === '/admin/slovarji') { @@ -280,12 +282,16 @@ function initAdmin() { } } - if (/\/admin\/slovarji\/\d+\/podatki/.test(currentPagePath)) { + if ( + /\/admin\/slovarji\/\d+\/podatki/.test(currentPagePath) || + /\/slovarji\/\d+\/podatki/.test(currentPagePath) + ) { const formEl = document.getElementById('admin-description') const imgTrashIcon = document.querySelectorAll('.delete-author-btn') const inputNewAuthorEl = document.getElementById('input-new-author') const inputNewAreaEl = document.getElementById('input-new-area') const dictSideMenu = document.querySelector('.admin-nav-content') + $('.without-addition').on('change', enableButton) unsavedData(formEl, dictSideMenu) formEl.addEventListener('input', enableButton) if (imgTrashIcon !== null) { @@ -946,7 +952,7 @@ function mobileMoveContent() { // primaryButton.style.marginRight = '10px' primaryButton.style.whiteSpace = 'nowrap' } - navTitle.textContent = siteHeadingTextContent + if (navTitle) navTitle.textContent = siteHeadingTextContent siteHeading.style.display = 'none' } if (document.body.clientWidth > 1200) { @@ -966,12 +972,14 @@ function mobileMoveContent() { primaryButton.style.whiteSpace = '' } - if ( - currentPagePath.includes('slovarji') && - !currentPagePath.includes('admin') - ) - navTitle.textContent = i18next.t('Urejanje') - else navTitle.textContent = i18next.t('Administracija') + if (navTitle) { + if ( + currentPagePath.includes('slovarji') && + !currentPagePath.includes('admin') + ) + navTitle.textContent = i18next.t('Urejanje') + else navTitle.textContent = i18next.t('Administracija') + } siteHeading.style.display = 'block' } } @@ -1180,7 +1188,7 @@ $('.summernote').summernote({ const profileForm = document.getElementById('profileForm') if (profileForm) { - profileForm.addEventListener('change', e => { + profileForm.addEventListener('input', e => { enableButton() }) @@ -1189,12 +1197,30 @@ $('.summernote').summernote({ const data = Object.fromEntries(new FormData(e.target)) + const notifyAction = () => { + document.getElementById('fpi-text').innerHTML = i18next.t( + 'Izpolnite vsa prazna polja.' + ) + $('#reset-pass-info').modal('show') + } + if (data.numberOfHits) { await handleUpdateHitsPerPage(data.numberOfHits) } - if (data.name && data.surname) { - await handleUpdateUsersName(data.name, data.surname) + if (data.firstName && data.lastName && data.email) { + await handleUpdateBasicData(data) + } else if (location.pathname === '/moj-racun') { + // if missing the required data on endpoint /moj-racun, notify! + notifyAction() + return + } + + if (data.passwordOld && data.passwordNew && data.passwordNewRepeat) { + await handleUpdatePassword(data) + } else if (location.pathname === '/spremeni-geslo') { + // if missing the required data on endpoint /spremeni-geslo, notify! + notifyAction() } // console.log(data) @@ -1204,23 +1230,60 @@ $('.summernote').summernote({ async function handleUpdateHitsPerPage(hitAmount) { try { await axios.post('/api/v1/users/hitsPerPage', { hitAmount }) - } catch (error) { - // console.log(error) - } finally { location.reload(true) + } catch (error) { + displayError(error) } } - async function handleUpdateUsersName(name, surname) { + async function handleUpdateBasicData(payload) { try { - await axios.post('/api/v1/users/nameAndSurname', { - name, - surname - }) - } catch (error) { - // console.log(error) - } finally { + if (!validator.isEmail(payload.email)) { + document.getElementById('fpi-text').innerHTML = + i18next.t('Neveljavna e-pošta') + $('#reset-pass-info').modal('show') + return + } + await axios.post('/api/v1/users/basic-data', payload) location.reload(true) + } catch (error) { + displayError(error) } } + + async function handleUpdatePassword(payload) { + try { + if (payload.passwordNew !== payload.passwordNewRepeat) { + document.getElementById('fpi-text').innerHTML = i18next.t( + 'Gesli se ne ujemata' + ) + $('#reset-pass-info').modal('show') + return + } + + if (!validator.isLength(payload.passwordNew, { min: 8 })) { + document.getElementById('fpi-text').innerHTML = + i18next.t('Geslo je prekratko') + $('#reset-pass-info').modal('show') + return + } + + await axios.post('/api/v1/users/password', payload) + location.reload(true) + } catch (error) { + displayError(error) + } + } + + function displayError(error) { + let message = i18next.t('Prišlo je do napake.') + if (error.response) { + message = error.response.data + } else if (error.request) { + message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.') + } + + document.getElementById('fpi-text').textContent = message + $('#reset-pass-info').modal('show') + } } diff --git a/express/public/javascripts/demo-paginacija.js b/express/public/javascripts/demo-paginacija.js deleted file mode 100644 index 3bf44e4..0000000 --- a/express/public/javascripts/demo-paginacija.js +++ /dev/null @@ -1,188 +0,0 @@ -/* global axios */ - -// Priporočam uporabo block scopa okoli paginacijske logike za čimvečjo izolacijo. -{ - // Referenca na element, kamor se izrisuje seznam rezultatov. - const resultsListEl = document.getElementById('page-results') - - // Paginacijo inicializiraš s klicem funkcije initPagination: - // 1. parameter: id pager elementa. V demo-paginacija.pug je to #pagination. - // UPDATE: Sedaj sprejme tudi array idjev, če je pager kontrolerjev več (npr. en zgoraj (#pagination-top), en spodaj (#pagination-bottom)). - // 2. parameter: callback funkcijo, ki jo pager pokliče vsakič, ko uporabnik zahteva novo stran. Poliče jo s številko zahtevane strani. - // vrnjena vrednost: funkcija, ki jo (kasneje) kličeš za posodobitev pagerja. Tu jo poimenujem updateDemoPager. - const updateDemoPager = initPagination( - ['pagination-top', 'pagination-bottom'], - onPageChange - ) - // Za samo en kontroler je bilo: - // const updateDemoPager = initPagination('pagination', onPageChange) - - // Fukncija, prejme številko nove strani in naj: - // 1. Pridobi podatke nove strani. - // 2. Izriše seznam elementov te strani. - // 3. Posodobi pager, tako, da kliče funkcijo, ki jo je vrnil klic initPagination (updateDemoPager) z novo stranjo in številom vseh strani. - async function onPageChange(newPage) { - try { - const { page, numberOfAllPages, results } = await getDataForPage(newPage) - removeAllChildNodes(resultsListEl) - renderResults(results) - updateDemoPager(page, numberOfAllPages) - } catch (error) { - let message = 'Prišlo je do napake.' - if (error.response?.data) { - message = error.response.data - } else if (error.request) { - message = 'Strežnik ni dosegljiv. Poskusite kasneje.' - } - alert(message) - updateDemoPager() - } - } - - // Primer helper funkcije za pridobitev podatkov želene strani. - async function getDataForPage(page) { - const url = `/api/v1/demo-paginacija/list?p=${page}` - const { data } = await axios.get(url) - return data - } - - // Primer helper funkcije za izris seznama novih podatkov. - function renderResults(results) { - results.forEach(result => { - const newListEl = document.createElement('li') - const textNode1 = document.createTextNode('Zanimiva vrednost: ') - const boldedEl = document.createElement('b') - boldedEl.textContent = result.zanimivo - const textNode2 = document.createTextNode( - `. Totalno nezanimivo: ${result.nezanimivo1} in ${result.nezanimivo2}` - ) - - newListEl.append(textNode1, boldedEl, textNode2) - resultsListEl.appendChild(newListEl) - }) - } -} - -/** ****************************************************************************************************************************************** **\ - * Koda od tu navzdol za vaju ni relevantna. Tu je, da dela zgornja koda. Za realno uporabo sem je skopiral tudi v public/javascripts/scripts.js * - * Na vsaki strani, kjer bo paginacija, jo inicializiraj in uporabljaj po zgledu zgornje kode. * -\** ****************************************************************************************************************************************** **/ - -function initPagination(paginationRootElIds, onPageChange, currentPage = 1) { - let reqLock = false - let numOfAllPages - const backwardBtnEls = [] - const forwardBtnEls = [] - const pageInputEls = [] - const pagesCountDisplayEls = [] - - if (Array.isArray(paginationRootElIds)) { - paginationRootElIds.forEach(id => initControls(id)) - } else { - initControls(paginationRootElIds) - } - const allControlEls = [...backwardBtnEls, ...forwardBtnEls, ...pageInputEls] - - function initControls(paginationRootElId) { - const rootEl = document.getElementById(paginationRootElId) - const btnFirstPage = rootEl.querySelector('.first-page') - const btnPreviousPage = rootEl.querySelector('.previous-page') - const btnNextPage = rootEl.querySelector('.next-page') - const btnLastPage = rootEl.querySelector('.last-page') - const formEl = rootEl.querySelector('form') - const pageInputEl = formEl.querySelector('input') - const pagesCountDisplayEl = formEl.querySelector('.pages-total') - - numOfAllPages = +pagesCountDisplayEl.textContent - - backwardBtnEls.push(btnFirstPage, btnPreviousPage) - forwardBtnEls.push(btnNextPage, btnLastPage) - pageInputEls.push(pageInputEl) - pagesCountDisplayEls.push(pagesCountDisplayEl) - - rootEl.addEventListener('click', e => { - handleButtonClick(e, paginationRootElId) - }) - formEl.addEventListener('submit', e => handleFormSubmit(e, pageInputEl)) - } - - function handleButtonClick({ target }, paginationRootElId) { - if (reqLock) return - - const buttonEl = target.closest(`#${paginationRootElId} button`) - if (!buttonEl) return - - if (buttonEl.classList.contains('first-page')) { - if (currentPage === 1) return - enableLock() - onPageChange(1) - } else if (buttonEl.classList.contains('previous-page')) { - if (currentPage === 1) return - enableLock() - onPageChange(currentPage - 1) - } else if (buttonEl.classList.contains('next-page')) { - if (currentPage === numOfAllPages) return - enableLock() - onPageChange(currentPage + 1) - } else if (buttonEl.classList.contains('last-page')) { - if (currentPage === numOfAllPages) return - enableLock() - onPageChange(numOfAllPages) - } - } - - function handleFormSubmit(e, pageInputEl) { - e.preventDefault() - if (reqLock) return - - const inputValue = +pageInputEl.value - if (!(inputValue > 0 && inputValue <= numOfAllPages)) { - alert('Nepravilna vrednost strani') - pageInputEl.value = currentPage - return - } - - enableLock() - onPageChange(inputValue) - } - - function enableLock() { - reqLock = true - allControlEls.forEach(el => (el.disabled = true)) - } - - function disableLock() { - reqLock = false - allControlEls.forEach(el => (el.disabled = false)) - } - - function updatePagerUi(newCurrentPage, newNumOfAllPages) { - disableLock() - if (!newCurrentPage) return - - currentPage = newCurrentPage - pageInputEls.forEach(el => (el.value = newCurrentPage)) - pagesCountDisplayEls.forEach(el => (el.textContent = newNumOfAllPages)) - - if (newCurrentPage === 1) { - backwardBtnEls.forEach(el => (el.disabled = true)) - } else { - backwardBtnEls.forEach(el => (el.disabled = false)) - } - - if (newCurrentPage === newNumOfAllPages) { - forwardBtnEls.forEach(el => (el.disabled = true)) - } else { - forwardBtnEls.forEach(el => (el.disabled = false)) - } - } - - return updatePagerUi -} - -// Helper function to easily remove all child nodes. Useful for pagination. -function removeAllChildNodes(parent) { - while (parent.firstChild) { - parent.removeChild(parent.firstChild) - } -} diff --git a/express/public/javascripts/dictionaries.js b/express/public/javascripts/dictionaries.js index e91e116..7614783 100644 --- a/express/public/javascripts/dictionaries.js +++ b/express/public/javascripts/dictionaries.js @@ -845,7 +845,7 @@ function initDictionaries() { } } if (listForeignSynonyms.length) { - if (el.synonym != null) + if (el.synonym != null) { el.synonym.forEach(element => { if (element.length) { // eslint-disable-next-line @@ -861,6 +861,7 @@ function initDictionaries() { selectSyn.append(newOption).trigger('change') } }) + } } } }) @@ -2035,19 +2036,21 @@ function loadPreview(info) { } const languageContainers = document.querySelectorAll('.preview-one-language') - const langLine = document.querySelector('.language-line') + const langLine = document.querySelector('.start-line') if (languageContainers) { const arrLang = Array.from(languageContainers) arrLang.forEach(el => { const arrChildren = Array.from(el.children) if (arrChildren.filter(e => e.classList.contains('d-none')).length > 2) { el.classList.add('d-none') - langLine.classList.add('d-none') - } else { - el.classList.remove('d-none') - langLine.classList.remove('d-none') - } + } else el.classList.remove('d-none') }) + if ( + arrLang.filter(el => el.classList.contains('d-none')).length >= + arrLang.length + ) { + langLine.classList.add('d-none') + } else langLine.classList.remove('d-none') } changeCollapsedContent() } @@ -2110,10 +2113,10 @@ function changeVersionList(versions) { // eslint-disable-next-line new bootstrap.Tooltip(dateLabel, { title: - i18next.t('Verzija') + - `${el.version}
` + + i18next.t('Verzija:') + + ` ${el.version}
` + i18next.t('Avtor:') + - `${el.version_author}` + ` ${el.version_author}` }) allDatesEl.append(dateRadio) allDatesEl.appendChild(dateLabel) @@ -2133,9 +2136,9 @@ function setLatestVersion(data) { const tooltip = new bootstrap.Tooltip(latestVersionLabel, { title: i18next.t('Verzija:') + - `${data.version}
` + + ` ${data.version}
` + i18next.t('Avtor:') + - `${data.version_author}`, + ` ${data.version_author}`, customClass: 'dark-gray-tooltip', html: true, placement: 'bottom' @@ -2823,6 +2826,12 @@ function deleteContentData( linkType.value = 'related' linkText.value = '' } + if (listForeignDefinitions) { + const foreignDefinitionsFields = document.querySelectorAll( + '.foreign-definition-el' + ) + foreignDefinitionsFields.forEach(el => (el.value = '')) + } } function activateMe(term) { diff --git a/express/public/javascripts/dictionary-import-extraction.js b/express/public/javascripts/dictionary-import-extraction.js index 2af6c4e..f2f1a58 100644 --- a/express/public/javascripts/dictionary-import-extraction.js +++ b/express/public/javascripts/dictionary-import-extraction.js @@ -62,6 +62,10 @@ } function renderResults(results) { + const tableContainer = document.querySelector( + '.list-terminology-candidates' + ) + tableContainer.classList.remove('d-none') results.forEach(([sequentialCount, candidate]) => { const rowEl = document.createElement('tr') const tdId = document.createElement('td') diff --git a/express/public/javascripts/extraction-files-edit.js b/express/public/javascripts/extraction-files-edit.js index ae9a7fb..d159ced 100644 --- a/express/public/javascripts/extraction-files-edit.js +++ b/express/public/javascripts/extraction-files-edit.js @@ -197,7 +197,7 @@ function updateFileListEl(fileListEl, { status, fileStats, index }) { deleteImgEl.alt = '' const deleteSpanEl = document.createElement('span') deleteSpanEl.className = 'ms-2' - deleteSpanEl.textContent = 'Briši' + deleteSpanEl.textContent = i18next.t('Briši') const deleteButtonEl = document.createElement('button') deleteButtonEl.className = 'p-0 delete-file delete-btn-table' deleteButtonEl.append(deleteImgEl, deleteSpanEl) diff --git a/express/public/javascripts/extraction-poc-files-edit.js b/express/public/javascripts/extraction-poc-files-edit.js deleted file mode 100644 index 534c4ad..0000000 --- a/express/public/javascripts/extraction-poc-files-edit.js +++ /dev/null @@ -1,128 +0,0 @@ -/* global axios */ - -const fileUploadForm = document.forms['upload-files'] -const fileInputEl = fileUploadForm.querySelector('input[type="file"]') -const messageContainerEl = document.getElementById('messages') -const filesListEl = document.getElementById('files-list') - -const extractionId = +fileUploadForm.extractionId.value -let apiEndpointBase -switch (location.pathname.split('/').at(-1)) { - case 'besedila': - apiEndpointBase = `/api/v1/extraction/${extractionId}/documents` - break - - case 'stop-termini': - apiEndpointBase = `/api/v1/extraction/${extractionId}/stop-terms` - break - - default: - throw Error("apiEndpointBase couldn't be determined") -} - -fileInputEl.addEventListener('change', submitFiles) -filesListEl.addEventListener('click', handleFileClick) - -async function submitFiles() { - // TODO Lock additional submits for the duration of this function execution? - const MAX_FILE_SIZE = 10 ** 9 // 1 GB - const failedUploads = [] - - displaySpinner() - - for (const file of fileInputEl.files) { - if (file.size > MAX_FILE_SIZE) { - const failedUpload = { - filename: file.name, - message: 'File too large. Must not be over 1 GB.' - } - failedUploads.push(failedUpload) - continue - } - - const payload = new FormData() - payload.set(fileInputEl.name, file) - try { - await axios.put(apiEndpointBase, payload) - } catch (error) { - const failedUpload = { - filename: file.name, - message: error.response.data - } - failedUploads.push(failedUpload) - } - } - - try { - const { data: files } = await axios.get(apiEndpointBase) - updateFilesList(files) - } catch { - alert('Pri posodobljanju seznama naloženih datotek je prišlo do napake.') - } - - fileInputEl.value = '' - displayFailedUploads(failedUploads) - hideSpinner() -} - -function displaySpinner() { - const messageEl = document.createElement('li') - messageEl.textContent = 'Spinner on' - messageContainerEl.appendChild(messageEl) -} - -function hideSpinner() { - const messageEl = document.createElement('li') - messageEl.textContent = 'Spinner off' - messageContainerEl.appendChild(messageEl) -} - -function updateFilesList(files) { - removeAllChildNodes(filesListEl) - files.forEach(({ filename, size, timeModified }) => { - const fileEl = document.createElement('li') - const filenameSpanEl = document.createElement('span') - filenameSpanEl.className = 'filename' - filenameSpanEl.textContent = filename - const formattedDate = new Date(timeModified).toLocaleDateString('sl-SL') - const deleteButtonEl = document.createElement('a') - deleteButtonEl.className = 'delete-file' - deleteButtonEl.href = '#' - deleteButtonEl.textContent = 'BRIŠI' - fileEl.append( - 'DATOTEKA - Ime: ', - filenameSpanEl, - `, velikost: ${size}, datum: ${formattedDate} `, - deleteButtonEl - ) - filesListEl.appendChild(fileEl) - }) -} - -function displayFailedUploads(failedUploads) { - failedUploads.forEach(({ filename, message }) => { - const messageEl = document.createElement('li') - messageEl.textContent = `NAPAKA - Ime datoteke: ${filename}, razlog: ${message}` - messageContainerEl.appendChild(messageEl) - }) -} - -async function handleFileClick(e) { - if (e.target.closest('.delete-file')) { - const fileEl = e.target.closest('li') - const filename = fileEl.querySelector('.filename').textContent - try { - await axios.delete(`${apiEndpointBase}/${filename}`) - fileEl.remove() - } catch { - alert('Pri brisanju datoteke je prišlo do napake.') - } - } -} - -// Don't copy this one into final JS. It's already defined in scripts.js -function removeAllChildNodes(parent) { - while (parent.firstChild) { - parent.removeChild(parent.firstChild) - } -} diff --git a/express/public/javascripts/extraction-poc-list.js b/express/public/javascripts/extraction-poc-list.js deleted file mode 100644 index dd785f5..0000000 --- a/express/public/javascripts/extraction-poc-list.js +++ /dev/null @@ -1,22 +0,0 @@ -/* global axios */ - -const extractionListEl = document.getElementById('extraction-list') - -extractionListEl.addEventListener('click', onListClick) - -async function onListClick({ target }) { - if (target.classList.contains('btn-delete')) { - const extractionEl = target.closest('li') - const extractionId = extractionEl.dataset.id - await axios.delete(`/api/v1/extraction/${extractionId}`) - extractionEl.remove() - } else if (target.classList.contains('btn-begin')) { - const extractionEl = target.closest('li') - const extractionId = extractionEl.dataset.id - await axios.put(`/api/v1/extraction/${extractionId}/begin`) - } else if (target.classList.contains('btn-duplicate')) { - const extractionEl = target.closest('li') - const extractionId = extractionEl.dataset.id - await axios.post(`/api/v1/extraction/${extractionId}/duplicate`) - } -} diff --git a/express/public/javascripts/extraction-poc-oss-edit.js b/express/public/javascripts/extraction-poc-oss-edit.js deleted file mode 100644 index 818253d..0000000 --- a/express/public/javascripts/extraction-poc-oss-edit.js +++ /dev/null @@ -1,95 +0,0 @@ -/* global $, axios */ - -$('.pick-multiple').select2() -$('.enter-multiple').select2({ - tags: true -}) - -const editStopTermsLink = document.getElementById('edit-stop-terms') -const searchButton = document.getElementById('search-btn') -const searchResultEl = document.getElementById('search-result') -const messageContainerEl = document.getElementById('messages') -const formEl = document.forms[0] -const extractionId = +location.pathname.split('/').at(-1) - -editStopTermsLink.addEventListener('click', saveOssParamsFirst) -searchButton.addEventListener('click', handleSearch) -searchResultEl.addEventListener('click', handleSearchResultsClick) - -async function saveOssParamsFirst() { - const payload = new URLSearchParams(new FormData(formEl)) - navigator.sendBeacon( - `/api/v1/extraction/${extractionId}/oss-save-params`, - payload - ) -} - -async function handleSearch() { - displaySpinner() - try { - const { data } = await submitSearch() - displaySearchResults(data) - } catch { - handleSearchError() - } - hideSpinner() -} - -function displaySpinner() { - const messageEl = document.createElement('li') - messageEl.textContent = 'Spinner on' - messageContainerEl.appendChild(messageEl) -} - -function hideSpinner() { - const messageEl = document.createElement('li') - messageEl.textContent = 'Spinner off' - messageContainerEl.appendChild(messageEl) -} - -function handleSearchError() { - const messageEl = document.createElement('li') - messageEl.textContent = 'Notify the user of error that occured during search' - messageContainerEl.appendChild(messageEl) -} - -async function submitSearch() { - const payload = new URLSearchParams(new FormData(formEl)) - return await axios.put( - `/api/v1/extraction/${extractionId}/oss-search`, - payload - ) -} - -function displaySearchResults({ documentCount, canSave }) { - removeAllChildNodes(searchResultEl) - searchResultEl.textContent = `Število dokumentov: ${documentCount}` - if (canSave) { - const saveButton = document.createElement('button') - saveButton.id = 'save-params' - saveButton.textContent = 'Shrani' - searchResultEl.append(saveButton) - } -} - -function handleSearchResultsClick({ target }) { - if (target.closest('#save-params')) confirmParams() -} - -async function confirmParams() { - displaySpinner() - try { - await axios.put(`/api/v1/extraction/${extractionId}/oss-confirm-params`) - location = '../poc' - } catch { - alert('Error saving params') - hideSpinner() - } -} - -// Don't copy this one into final JS. It's already defined in scripts.js -function removeAllChildNodes(parent) { - while (parent.firstChild) { - parent.removeChild(parent.firstChild) - } -} diff --git a/express/public/javascripts/extraction-poc-term-candidates.js b/express/public/javascripts/extraction-poc-term-candidates.js deleted file mode 100644 index 4c4496b..0000000 --- a/express/public/javascripts/extraction-poc-term-candidates.js +++ /dev/null @@ -1,144 +0,0 @@ -/* global termCandidates, hitsPerPage, numberOfAllPages */ -{ - const resultsListEl = document.getElementById('page-results') - - const updateDemoPager = initPagination('pagination', onPageChange) - - function onPageChange(newPage) { - const results = getDataForPage(newPage) - removeAllChildNodes(resultsListEl) - renderResults(results) - updateDemoPager(newPage, numberOfAllPages) - } - - function getDataForPage(page) { - const sliceStart = (page - 1) * hitsPerPage - const sliceEnd = page * hitsPerPage - const onePageOfTermCandidates = termCandidates.slice(sliceStart, sliceEnd) - const data = onePageOfTermCandidates.map((candidate, index) => { - const sequentialCount = sliceStart + index + 1 - return [sequentialCount, candidate] - }) - return data - } - - function renderResults(results) { - results.forEach(([sequentialCount, candidate]) => { - const newListEl = document.createElement('li') - newListEl.textContent = `[${sequentialCount}] ${JSON.stringify( - candidate - )}` - resultsListEl.appendChild(newListEl) - }) - } -} - -// Below code is copied from scripts.js, so it will already be available on the real page. No need to copy it there also. -function initPagination(paginationRootElId, onPageChange, currentPage = 1) { - const rootEl = document.getElementById(paginationRootElId) - const btnFirstPage = rootEl.querySelector('.first-page') - const btnPreviousPage = rootEl.querySelector('.previous-page') - const btnNextPage = rootEl.querySelector('.next-page') - const btnLastPage = rootEl.querySelector('.last-page') - const formEl = rootEl.querySelector('form') - const pageInputEl = formEl.querySelector('input') - const pagesCountDisplayEl = formEl.querySelector('.pages-total') - - let reqLock = false - - rootEl.addEventListener('click', handleButtonClick) - formEl.addEventListener('submit', handleFormSubmit) - - function handleButtonClick({ target }) { - if (reqLock) return - - const buttonEl = target.closest(`#${paginationRootElId} button`) - if (!buttonEl) return - const numOfAllPages = +pagesCountDisplayEl.textContent - - if (buttonEl.classList.contains('first-page')) { - if (currentPage === 1) return - enableLock() - onPageChange(1) - } else if (buttonEl.classList.contains('previous-page')) { - if (currentPage === 1) return - enableLock() - onPageChange(currentPage - 1) - } else if (buttonEl.classList.contains('next-page')) { - if (currentPage === numOfAllPages) return - enableLock() - onPageChange(currentPage + 1) - } else if (buttonEl.classList.contains('last-page')) { - if (currentPage === numOfAllPages) return - enableLock() - onPageChange(numOfAllPages) - } - } - - function handleFormSubmit(e) { - e.preventDefault() - if (reqLock) return - - const inputValue = +pageInputEl.value - if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) { - alert('Nepravilna vrednost strani') - pageInputEl.value = currentPage - return - } - - enableLock() - onPageChange(inputValue) - } - - function enableLock() { - reqLock = true - btnFirstPage.disabled = true - btnPreviousPage.disabled = true - btnNextPage.disabled = true - btnLastPage.disabled = true - pageInputEl.disabled = true - } - - function disableLock() { - reqLock = false - btnFirstPage.disabled = false - btnPreviousPage.disabled = false - btnNextPage.disabled = false - btnLastPage.disabled = false - pageInputEl.disabled = false - } - - function updatePagerUi(newCurrentPage, newNumOfAllPages) { - disableLock() - if (!newCurrentPage) return - - currentPage = newCurrentPage - pageInputEl.value = newCurrentPage - pagesCountDisplayEl.textContent = newNumOfAllPages - - if (newCurrentPage === 1) { - btnFirstPage.disabled = true - btnPreviousPage.disabled = true - } else { - btnFirstPage.disabled = false - btnPreviousPage.disabled = false - } - - if (newCurrentPage === newNumOfAllPages) { - btnNextPage.disabled = true - btnLastPage.disabled = true - } else { - btnNextPage.disabled = false - btnLastPage.disabled = false - } - } - - return updatePagerUi -} - -// Helper function to easily remove all child nodes. Useful for pagination. -function removeAllChildNodes(parent) { - while (parent.firstChild) { - parent.removeChild(parent.firstChild) - } -} diff --git a/express/public/javascripts/profile-scripts.js b/express/public/javascripts/profile-scripts.js index 592df16..d7b0100 100644 --- a/express/public/javascripts/profile-scripts.js +++ b/express/public/javascripts/profile-scripts.js @@ -1,11 +1,34 @@ -/* global isI18nReady */ +/* global isI18nReady, i18next, $, axios */ isI18nReady.then(t => { const alertText = document.querySelector('#alert-text') alertText.textContent = t( - 'Ali želite zbrisati ta profil. S tem bodo izbrisani vsi podatki, ki ste jih ustvarili' + 'Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.' ) }) const redConfirm = document.querySelector('#modal-use-btn') redConfirm.style.backgroundColor = '#AC7171' +document.querySelector('#modal-alert-label').style.color = '#AC7171' + +redConfirm.addEventListener('click', async () => { + // TODO: Optimize, create a common helper function for example + function displayError(error) { + let message = i18next.t('Prišlo je do napake.') + if (error.response) { + message = error.response.data + } else if (error.request) { + message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.') + } + + document.querySelector('#info-text').textContent = message + $('#info-modal').modal('show') + } + + try { + await axios.delete('/api/v1/users/current') + window.location = '/' + } catch (error) { + displayError(error) + } +}) diff --git a/express/public/javascripts/reset-password.js b/express/public/javascripts/reset-password.js index 921424a..2b37565 100644 --- a/express/public/javascripts/reset-password.js +++ b/express/public/javascripts/reset-password.js @@ -1,59 +1,66 @@ -/* global $, axios, i18next */ +/* global $, axios, validator, i18next */ // Function to verify password and repeat password function verifyPassword() { const password = document.getElementById('reset-password').value const repeatPassword = document.getElementById('reset-password-repeat').value + if (password !== repeatPassword) { - alert(i18next.t('Gesli se ne ujemata')) // 'Passwords do not match') + // alert(i18next.t('Gesli se ne ujemata')) // 'Passwords do not match') + document.querySelector('#reset-password-error').textContent = i18next.t( + 'Gesli se ne ujemata' + ) + document.querySelector('#reset-password-error').style.visibility = 'visible' return false } + + if (!validator.isLength(password, { min: 8 })) { + // alert(i18next.t('Geslo je prekratko')) // 'Passwords do not match') + document.querySelector('#reset-password-error').textContent = + i18next.t('Geslo je prekratko') + document.querySelector('#reset-password-error').style.visibility = 'visible' + return false + } + + document.querySelector('#reset-password-error').style.visibility = 'invisible' return true } // Handle submit event document .querySelector('#reset-and-redirect') - .addEventListener('submit', event => { + .addEventListener('submit', async event => { event.preventDefault() if (verifyPassword()) { + const token = document.getElementById('token').value const password = document.getElementById('reset-password').value - const repeatPassword = document.getElementById( + const passwordRepeat = document.getElementById( 'reset-password-repeat' ).value - const token = document.getElementById('token').value // const email = document.getElementById('email').value // const data = { password, repeatPassword, token, email } - axios - .post('/api/v1/users/reset-passwordPLACEHOLDER_URL', { + try { + await axios.post('/api/v1/users/reset-password-submit', { + token, password, - repeatPassword, - token - }) - .then(response => { - if (response.data.success) { - $('#reset-pass-info').modal('show') - // window.location = '/' - } else { - alert(response.data.message) - } - }) - .catch(error => { - // Very unlikely, but can happen - alert(i18next.t('Napaka na strežniku')) - console.log(error) - // DEBUG SUCCESS DUE TO NO ENDPOINT $('#reset-pass-info').modal('show') <<- REMOVE + passwordRepeat }) + + window.location = '/' + } catch (error) { + displayError(error) + } } }) -// Handle cancel event -document.querySelector('#cancel-btn').addEventListener('click', event => { - event.preventDefault() - window.location = '/' -}) +function displayError(error) { + let message = i18next.t('Prišlo je do napake.') + if (error.response) { + message = error.response.data + } else if (error.request) { + message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.') + } -// Handle redirect on success -document.querySelector('#modal-fp-info-close').addEventListener('click', () => { - window.location = '/' -}) + document.querySelector('#fpi-text.normal-gray').textContent = message + $('#reset-pass-info').modal('show') +} diff --git a/express/public/javascripts/scripts.js b/express/public/javascripts/scripts.js index cf31356..06b3e53 100644 --- a/express/public/javascripts/scripts.js +++ b/express/public/javascripts/scripts.js @@ -953,85 +953,45 @@ if (registerSwithcButton) { $(document).ready(function () { // TODO refactor wth specific select2 - $('.select-search-field').select2({}) - initSelect2('.select-domain-field', 'Področje') - initSelect2('.select-src-lang-field', 'Jezik iskanja') - initSelect2('.select-dest-lang-field', 'Ciljni jezik') - initSelect2('.select-dict-field', 'Slovar') - initSelect2('.select-source-field', 'Vir') + isI18nReady.then(t => { + $('.select-search-field').select2({}) - $('b[role="presentation"]').hide() - $('.select2-selection__arrow').append( - 'V' - ) + initSelect2('.select-domain-field', t('Področje')) + initSelect2('.select-src-lang-field', t('Jezik iskanja')) + initSelect2('.select-dest-lang-field', t('Ciljni jezik')) + initSelect2('.select-dict-field', t('Slovar')) + initSelect2('.select-source-field', t('Vir')) - $('.select-search-field').on('select2:select', function (e) { - selectActiveElementFromInput(e) - // console.log(activeInput) - // console.log(e.target.parentNode.children[2]) - // const label = e.target.parentNode.children[2] + $('b[role="presentation"]').hide() + $('.select2-selection__arrow').append( + 'V' + ) - // console.log(activeInput) - // console.log(activeInput) - activeInput.addTag(e.params.data._resultId) - activeInput.labelJump() - }) + $('.select-search-field').on('select2:select', function (e) { + selectActiveElementFromInput(e) + // console.log(activeInput) + // console.log(e.target.parentNode.children[2]) + // const label = e.target.parentNode.children[2] - $('.select-search-field').on('select2:unselect', function (e) { - selectActiveElementFromInput(e) - // console.log(activeInput) - // console.log(activeInput) - activeInput.removeTag(e.params.data._resultId) - activeInput.labelJump() - // console.log('DELETED ' + e) - }) + // console.log(activeInput) + // console.log(activeInput) + activeInput.addTag(e.params.data._resultId) + activeInput.labelJump() + }) - inputs.forEach(e => { - e.input = e.domElement.querySelector('.select2-search__field') - }) - }) + $('.select-search-field').on('select2:unselect', function (e) { + selectActiveElementFromInput(e) + // console.log(activeInput) + // console.log(activeInput) + activeInput.removeTag(e.params.data._resultId) + activeInput.labelJump() + // console.log('DELETED ' + e) + }) - /* TODO REMOVE OTHER SCRIPTS WHEN YOU FINISH MODULARIZING THINGS THAT COULD BE MODULARIZED */ - - $(document).ready(function () { - // TODO refactor wth specific select2 - $('.select-search-field').select2({}) - - initSelect2('.select-domain-field', 'Področje') - initSelect2('.select-src-lang-field', 'Jezik iskanja') - initSelect2('.select-dest-lang-field', 'Ciljni jezik') - initSelect2('.select-dict-field', 'Slovar') - initSelect2('.select-source-field', 'Vir') - - $('b[role="presentation"]').hide() - $('.select2-selection__arrow').append( - 'V' - ) - - $('.select-search-field').on('select2:select', function (e) { - selectActiveElementFromInput(e) - // console.log(activeInput) - // console.log(e.target.parentNode.children[2]) - // const label = e.target.parentNode.children[2] - - // console.log(activeInput) - // console.log(activeInput) - activeInput.addTag(e.params.data._resultId) - activeInput.labelJump() - }) - - $('.select-search-field').on('select2:unselect', function (e) { - selectActiveElementFromInput(e) - // console.log(activeInput) - // console.log(activeInput) - activeInput.removeTag(e.params.data._resultId) - activeInput.labelJump() - // console.log('DELETED ' + e) - }) - - inputs.forEach(e => { - e.input = e.domElement.querySelector('.select2-search__field') + inputs.forEach(e => { + e.input = e.domElement.querySelector('.select2-search__field') + }) }) }) @@ -1192,7 +1152,7 @@ const forgottenPasswordInvoker = new StateManagerInvoker(stateManager, () => { } function apllyDescriptionAccordingToState() { - $('#alert-text').text(stateManager.getState().fpassWindowDescription) + $('#fpi-text').text(stateManager.getState().fpassWindowDescription) } closeAllFPRelatedModals() @@ -1233,8 +1193,21 @@ function onCancelForgotPassword() { } function onSendForgottenEmailRequest() { + if (document.getElementById('forgot-pass-input').value === '') { + stateManager.setState({ + fpassWindowDescription: i18next.t( + 'Prosimo vnesite vaš elektronski naslov.' + ) + }) + + forgottenPasswordInvoker.setState({ + fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_ERROR + }) + return + } + axios - .post('/REPLACETHISDUMMYURL', { + .post('/api/v1/users/reset-password-init', { usernameOrEmail: document.getElementById('forgot-pass-input').value }) .then(res => { @@ -1253,11 +1226,18 @@ function onSendForgottenEmailRequest() { }) }) .catch(err => { - stateManager.setState({ - fpassWindowDescription: i18next.t( - 'Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.' - ) - }) + if (err.response && err.response.data) { + stateManager.setState({ + fpassWindowDescription: err.response.data + }) + } else { + stateManager.setState({ + fpassWindowDescription: i18next.t( + 'Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.' + ) + }) + } + forgottenPasswordInvoker.setState({ fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_ERROR }) diff --git a/express/public/javascripts/search-results.js b/express/public/javascripts/search-results.js index c8c9346..d200ab4 100644 --- a/express/public/javascripts/search-results.js +++ b/express/public/javascripts/search-results.js @@ -1,7 +1,7 @@ /* global $, axios, currentPagePath, initPagination, removeAllChildNodes, transferText, tooltipTriggerList, tooltipList, createTooltip, resetTooltipTriggerList, - transferTextExtended, i18next */ + transferTextExtended, isI18nReady, i18next */ // position correction functions @@ -278,20 +278,22 @@ window.addEventListener('load', () => { $('[data-toggle="tooltip"]').tooltip() }) -function handleProperTextDisplay() { - // const BROWSER_UNUSUAL_OFFSET = 17 - if (/\/iskanje/.test(currentPagePath)) { - transferText('Iskanje po slovarjih', true, 'site-heading') //, - // BROWSER_UNUSUAL_OFFSET - // ) - } else if (/\/termin/.test(currentPagePath)) { - transferText('', true, 'site-heading') // , BROWSER_UNUSUAL_OFFSET) +isI18nReady.then(t => { + function handleProperTextDisplay() { + // const BROWSER_UNUSUAL_OFFSET = 17 + if (/\/iskanje/.test(currentPagePath)) { + transferText(t('Iskanje po slovarjih'), true, 'site-heading') //, + // BROWSER_UNUSUAL_OFFSET + // ) + } else if (/\/termin/.test(currentPagePath)) { + transferText('', true, 'site-heading') // , BROWSER_UNUSUAL_OFFSET) + } } -} -window.addEventListener('resize', () => { + window.addEventListener('resize', () => { + handleProperTextDisplay() + adjustOffsetBy() + }) + handleProperTextDisplay() - adjustOffsetBy() }) - -handleProperTextDisplay() diff --git a/express/public/locales/en/core.json b/express/public/locales/en/core.json index 4294fc4..1457a78 100644 --- a/express/public/locales/en/core.json +++ b/express/public/locales/en/core.json @@ -222,7 +222,7 @@ "Modul za svetovanje pri terminoloških zagatah.": "Terminology consulting module.", "Modul za urejanje terminoloških slovarjev.": "Module for editing terminology dictionaries.", "MODULI": "MODULES", - "Moj Profil": "My Profile", + "Moj Račun": "My Account", "Moji slovarji": "My dictionaries", "Morebitne že obstoječe poimenovalne rešitve, če obstajajo.": "Any already existing naming solutions.", "Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.": "Any examples of term use in texts or links to text where the term is used.", @@ -234,7 +234,7 @@ "Na tem mestu lahko določite področne oznake, če želite posamezne termine v svojem terminološkem slovarju razvrstiti še podrobneje.": "Here you can define domain labels, if you want to classify individual terms in your terminology dictionary in more detail.", "Na tem mestu so zbrani vsi komentarji, povezani s terminološkim portalom.": "All comments related to the terminology portal can be found here.", "Na tem mestu so zbrani vsi komentarji, povezani z izbranim terminološkim slovarjem.": "All comments related to the selected terminology dictionary can be found here.", - "na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna ": "we a message with a link to verify your Termonology Portal user account to your e-mail address. The verification link is valid.", + "na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna ": "we have sent a message with a link to verify your Termonology Portal user account to your e-mail address. The verification link will be valid ", "Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.": "We sent a password reset link to your e-mail address. Please, check your inbox.", "Nabor vseh luščenj, s katerimi lahko uporabnik iz izbranih besedil pridobi sezname terminoloških kandidatov. Posamezne terminološke kandidate lahko preveri tudi v konkordančniku.": "All extractions the users can use to obtain a list of term candidates from the selected texts. The user can also check individual term candidates using the concordance tool.", "Nabor vseh luščenj, s pomočjo katerih uporabnik iz vhodnih besedil pridobi sezname terminoloških kandidatov in dostop do konkordančnika po teh besedilih.": "All extractions the users can use to obtain a list of term candidates from the selected texts and access to the concordance tools for these texts.", @@ -407,7 +407,7 @@ "POVEZANI TERMIN": "RELATED TERM", "POVEZANI TERMIN:": "RELATED TERM:", "POVEZAVA": "LINK", - "Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.": "The link is (no longer) valid. Please, request new password reset link.", + "Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.": "This link is invalid or expired. Please, request new password reset link.", "Povezave": "Links", "Povezave s portali": "Links to other portals", "Pozabljeno geslo": "Forgoten password", @@ -540,7 +540,7 @@ "Strokovni pregled": "Terminology review", "Strokovno pregledano": "Terminologically reviewed", "Struktura": "Structure", - "STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:": "STRUCTURE - ELEMENTS OF THA DICTIONARY ENTRY:", + "STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:": "STRUCTURE - ELEMENTS OF THE DICTIONARY ENTRY:", "Struktura slovarskega sestavka": "The structure of the dictionary entry", "Svetovalci": "Consultants", "Svetovalec": "Consultant", @@ -706,5 +706,35 @@ "ZVOK": "AUDIO", "ZVOK:": "AUDIO:", "titleTermsOfUse": "Terms of Use", - "titlePrivacyPolicy": "Privacy Policy" + "titlePrivacyPolicy": "Privacy Policy", + "userStatusregistered": "registered", + "userStatusactive": "active", + "userStatusinactive": "inactive", + "userStatusclosed": "closed", + "Nepravilno uporabniško ime ali elektronski naslov.": "Invalid user name or e-mail address.", + "Ponastavitev gesla": "Password reset", + "Uspešna ponastavitev gesla": "Password successfully reset", + "Vaše geslo je bilo uspešno ponastavljeno.": "Your password has been successfully reset.", + "Prosimo vnesite vaš elektronski naslov.": "Please, enter your e-mail address.", + "Sprememba elektronskega naslova": "Change e-mail address", + "Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ": "We have sent a message with a link to confirm the change of your e-mail address to your new e-mail adress. The verification link will be valid ", + "Sprememba elektronskega naslova - uspeh": "E-mail address change successful", + "Uspešno ste spremenili svoj elektronski naslov.": "You have successfully changed your e-mail address.", + "Elektronski naslov uporablja že drug uporabnik.": "This e-mail address is already used by another user.", + "Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.": "This link is invalid or expired. The e-mail address has not been changed.", + "Vaš uporabniški račun je bil uspešno izbrisan.": "Your user account has been successfully deleted.", + "Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.": "Do you really want to delete your account? If you delete your account, you will permanently lose access to all data you created.", + "Uspešno ste aktivirali svoj uporabniški račun in se prijavili.": "You have successfully activated your user account and signed in.", + "Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.": "This link is invalid or expired. Please, register again.", + "Nepravilno staro geslo.": "Invalid current password.", + "Sprememba gesla": "Change password", + "Izbrano uporabniško ime uporablja že drug uporabnik.": "This user name is already taken.", + "Izpolnite vsa prazna polja.": "Fill-in all empty fields.", + "Jezik iskanja": "Search language", + "Ciljni jezik": "Target language", + "Spremenjen": "Changed", + "Število slovarskih sestavkov": "Number of dictionary entries", + "VPRAŠANJE": "QUESTION", + "MNENJE": "OPINION", + "ANGLEŠKI PREVOD": "ENGLISH TRANSLATION" } diff --git a/express/public/locales/sl/core.json b/express/public/locales/sl/core.json index 8a5b2ba..878e8fc 100644 --- a/express/public/locales/sl/core.json +++ b/express/public/locales/sl/core.json @@ -222,7 +222,7 @@ "Modul za svetovanje pri terminoloških zagatah.": "Modul za svetovanje pri terminoloških zagatah.", "Modul za urejanje terminoloških slovarjev.": "Modul za urejanje terminoloških slovarjev.", "MODULI": "MODULI", - "Moj Profil": "Moj Profil", + "Moj Račun": "Moj Račun", "Moji slovarji": "Moji slovarji", "Morebitne že obstoječe poimenovalne rešitve, če obstajajo.": "Morebitne že obstoječe poimenovalne rešitve, če obstajajo.", "Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.": "Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.", @@ -706,5 +706,35 @@ "ZVOK": "ZVOK", "ZVOK:": "ZVOK:", "titleTermsOfUse": "Pogoji uporabe", - "titlePrivacyPolicy": "Politika zasebnosti" + "titlePrivacyPolicy": "Politika zasebnosti", + "userStatusregistered": "registriran", + "userStatusactive": "aktiven", + "userStatusinactive": "neaktiven", + "userStatusclosed": "zaprt", + "Nepravilno uporabniško ime ali elektronski naslov.": "Nepravilno uporabniško ime ali elektronski naslov.", + "Ponastavitev gesla": "Ponastavitev gesla", + "Uspešna ponastavitev gesla": "Uspešna ponastavitev gesla", + "Vaše geslo je bilo uspešno ponastavljeno.": "Vaše geslo je bilo uspešno ponastavljeno.", + "Prosimo vnesite vaš elektronski naslov.": "Prosimo vnesite vaš elektronski naslov.", + "Sprememba elektronskega naslova": "Sprememba elektronskega naslova", + "Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ": "Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ", + "Sprememba elektronskega naslova - uspeh": "Sprememba elektronskega naslova - uspeh", + "Uspešno ste spremenili svoj elektronski naslov.": "Uspešno ste spremenili svoj elektronski naslov.", + "Elektronski naslov uporablja že drug uporabnik.": "Elektronski naslov uporablja že drug uporabnik.", + "Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.": "Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.", + "Vaš uporabniški račun je bil uspešno izbrisan.": "Vaš uporabniški račun je bil uspešno izbrisan.", + "Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.": "Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.", + "Uspešno ste aktivirali svoj uporabniški račun in se prijavili.": "Uspešno ste aktivirali svoj uporabniški račun in se prijavili.", + "Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.": "Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.", + "Nepravilno staro geslo.": "Nepravilno staro geslo.", + "Sprememba gesla": "Sprememba gesla", + "Izbrano uporabniško ime uporablja že drug uporabnik.": "Izbrano uporabniško ime uporablja že drug uporabnik.", + "Izpolnite vsa prazna polja.": "Izpolnite vsa prazna polja.", + "Jezik iskanja": "Jezik iskanja", + "Ciljni jezik": "Ciljni jezik", + "Spremenjen": "Spremenjen", + "Število slovarskih sestavkov": "Število slovarskih sestavkov", + "VPRAŠANJE": "VPRAŠANJE", + "MNENJE": "MNENJE", + "ANGLEŠKI PREVOD": "ANGLEŠKI PREVOD" } diff --git a/express/routes/admin.js b/express/routes/admin.js index c873992..81f6ea7 100644 --- a/express/routes/admin.js +++ b/express/routes/admin.js @@ -6,18 +6,18 @@ const portal = require('../controllers/portals') // All routes require an authenticated user. router.use((req, res, next) => { if (req.isAuthenticated()) return next() - res.redirect('/') + res.redirect(303, '/') }) // Menu entry point. Redirect based on role. router.get('/', (req, res, next) => { const { user, baseUrl } = req if (user.hasRole('portal admin')) { - return res.redirect(`${baseUrl}/nastavitve/portal`) + return res.redirect(303, `${baseUrl}/nastavitve/portal`) } else if (user.hasRole('dictionaries admin')) { - return res.redirect(`${baseUrl}/slovarji`) + return res.redirect(303, `${baseUrl}/slovarji`) } - res.redirect('/') + res.redirect(303, '/') }) // Authorize endpoints for portal admin. @@ -25,14 +25,14 @@ router.use( ['/nastavitve', '/povezave', '/uporabniki', '/komentarji'], (req, res, next) => { if (req.user.hasRole('portal admin')) return next() - res.redirect('/') + res.redirect(303, '/') } ) // Authorize endpoints for dictionaries admin. -router.use(['/slovarji', '/podrocja'], (req, res, next) => { +router.use(['/slovarji', '/podpodrocja'], (req, res, next) => { if (req.user.hasRole('dictionaries admin')) return next() - res.redirect('/') + res.redirect(303, '/') }) router.get('/nastavitve/portal', portal.instanceSettings) @@ -59,9 +59,9 @@ router.get('/povezave/seznam/:portalId', portal.fetchSelectedLinkedDictionaries) router.post('/povezave/seznam/:portalId', portal.updateSelectedDictionaries) -router.get('/povezava/:portalId/urejanje', portal.fetchPortal) +router.get('/povezave/:portalId/urejanje', portal.fetchPortal) -router.post('/povezava/:portalId/urejanje', portal.updatePortal) +router.post('/povezave/:portalId/urejanje', portal.updatePortal) // Show a list of user's dictionaries. router.get('/slovarji', dictionary.listAdminDictionaries) diff --git a/express/routes/api/v1/comments.js b/express/routes/api/v1/comments.js index bbf10a9..8fe7134 100644 --- a/express/routes/api/v1/comments.js +++ b/express/routes/api/v1/comments.js @@ -3,20 +3,12 @@ const router = require('express-promise-router')() const { listComments, createComment, - seedComments, - clearComments, updateStatus } = require('../../../controllers/api/v1/comments') // List comments (one page). router.get('/', listComments) -// TEMP - Seed comments. -router.get('/seed/:commentCount', seedComments) - -// TEMP - Clear comments. -router.get('/clear', clearComments) - // All further routes are only available to authenticated users. router.use((req, res, next) => { if (req.isAuthenticated()) return next() diff --git a/express/routes/api/v1/consultancy.js b/express/routes/api/v1/consultancy.js index 799b257..23c2a4e 100644 --- a/express/routes/api/v1/consultancy.js +++ b/express/routes/api/v1/consultancy.js @@ -2,8 +2,6 @@ const router = require('express-promise-router')() const { assign, - listEntries, - listNewEntries, createQuestion, deleteQuestion, updateDomain, @@ -23,10 +21,6 @@ const { router.get('/entry-pagination', sendPaginationData) -router.get('/entry', listEntries) - -router.get('/new-entry', listNewEntries) - // All routes require an authenticated user. router.use((req, res, next) => { if (req.isAuthenticated()) return next() diff --git a/express/routes/api/v1/demo-paginacija.js b/express/routes/api/v1/demo-paginacija.js deleted file mode 100644 index 3328598..0000000 --- a/express/routes/api/v1/demo-paginacija.js +++ /dev/null @@ -1,7 +0,0 @@ -const router = require('express-promise-router')() -const demoPaginacija = require('../../../controllers/api/v1/demo-paginacija') - -// Tale endpoint vrača rezultate. -router.get('/list', demoPaginacija.list) - -module.exports = router diff --git a/express/routes/api/v1/dictionaries.js b/express/routes/api/v1/dictionaries.js index 4e073fc..8cb5d71 100644 --- a/express/routes/api/v1/dictionaries.js +++ b/express/routes/api/v1/dictionaries.js @@ -1,54 +1,91 @@ const router = require('express-promise-router')() const dictionaries = require('../../../controllers/api/v1/dictionaries') +const user = require('../../../middleware/user') // Update domain labels. router.post('/update-domain-labels', dictionaries.updateDomainLabels) // Update secondary domains -router.post('/renovateSecondaryDomains', dictionaries.renovateSecondaryDomains) +router.post( + '/renovateSecondaryDomains', + user.canAdministrateDictionaries, + dictionaries.renovateSecondaryDomains +) // Delete specific dictionary. -router.delete('/:dictionaryId', dictionaries.delete) +router.delete( + '/:dictionaryId', + user.canAdministrateDictionary, + dictionaries.delete +) // Change page in pagination -router.get('/listAllDictionaries', dictionaries.listDictionaries) +router.get( + '/listAllDictionaries', + user.canAdministrateDictionaries, + dictionaries.listDictionaries +) // Change page in pagination -router.get('/:dictionaryId/listDomainLabels', dictionaries.listDomainLabels) - -// Change page in pagination -router.get('/listSecondaryDomains', dictionaries.listSecondaryDomains) +router.get( + '/:dictionaryId/listDomainLabels', + user.canAdministrateDictionary, + dictionaries.listDomainLabels +) // Change page in pagination router.get( '/:dictionaryId/showImportFromFileForm', + user.canContentEdit, dictionaries.showImportFromFileForm ) // Change page in pagination router.get( '/:dictionaryId/showExportToFileForm', + user.canContentEdit, dictionaries.showExportToFileForm ) // Delete all entries of specific dictionary. -router.delete('/:dictionaryId/entries/all', dictionaries.deleteAllEntries) +router.delete( + '/:dictionaryId/entries/all', + user.canAdministrateDictionary, + dictionaries.deleteAllEntries +) // Publish all entries of specific dictionary. -router.put('/:dictionaryId/entries/all/publish', dictionaries.publishAllEntries) +router.put( + '/:dictionaryId/entries/all/publish', + user.canAdministrateDictionary, + dictionaries.publishAllEntries +) // Import term candidates from extraction. router.post( - '/:id/import-extraction/:extractionId', + '/:dictionaryId/import-extraction/:extractionId', + user.canContentEdit, dictionaries.importFromExtraction ) // Export dictionary into a file. -router.post('/:id/export-begin', dictionaries.exportBegin) +router.post( + '/:dictionaryId/export-begin', + user.canContentEdit, + dictionaries.exportBegin +) -router.get('/:dictionaryId/domainLabels', dictionaries.listFilteredDomainLabels) +router.get( + '/:dictionaryId/domainLabels', + user.canContentEdit, + dictionaries.listFilteredDomainLabels +) -router.get('/secondaryDomains', dictionaries.listSecondaryDomainData) +router.get( + '/secondaryDomains', + user.canAdministrateDictionaries, + dictionaries.listSecondaryDomainData +) module.exports = router diff --git a/express/routes/api/v1/entries.js b/express/routes/api/v1/entries.js index be99719..3be2612 100644 --- a/express/routes/api/v1/entries.js +++ b/express/routes/api/v1/entries.js @@ -1,5 +1,6 @@ // This page refers to dictionary entries, not consultancy or other entries const router = require('express-promise-router')() +const user = require('../../../middleware/user') const { getEntry, @@ -10,7 +11,8 @@ const { getEntryVersionSnapshot } = require('../../../controllers/api/v1/dictionaries') -// TODO Add authorization. +// All further routes are only available to authenticated users with dictionary content editing rights. +router.use(user.isAuthenticated, user.canContentEdit) // Render entry data for the selected entry router.get('/', getEntry) diff --git a/express/routes/api/v1/extraction.js b/express/routes/api/v1/extraction.js index 87d8a5a..ab5e4b2 100644 --- a/express/routes/api/v1/extraction.js +++ b/express/routes/api/v1/extraction.js @@ -1,54 +1,76 @@ const router = require('express-promise-router')() const extraction = require('../../../controllers/api/v1/extraction') +const { validateOwnership } = require('../../../controllers/extraction') -// TODO In final version test authentication and authorization. -// TODO Also try to lock file editing or other operations once extraction was started. +// TODO Try to lock file editing or other operations once extraction was started. + +// All further routes are only available to authenticated users. +router.use((req, res, next) => { + if (req.isAuthenticated()) return next() + res.status(400).end() +}) // Delete extraction. -router.delete('/:id', extraction.delete) +router.delete('/:id', validateOwnership, extraction.delete) // List documents. -router.get('/:id/documents', extraction.docsList) +router.get('/:id/documents', validateOwnership, extraction.docsList) // Update documents. -router.put('/:id/documents', extraction.docsUpdate) +router.put('/:id/documents', validateOwnership, extraction.docsUpdate) // Delete a specific document. -router.delete('/:id/documents/:filename', extraction.docDelete) +router.delete( + '/:id/documents/:filename', + validateOwnership, + extraction.docDelete +) // List stop term files. -router.get('/:id/stop-terms', extraction.stopTermsList) +router.get('/:id/stop-terms', validateOwnership, extraction.stopTermsList) // Update stop term files. -router.put('/:id/stop-terms', extraction.stopTermsUpdate) +router.put('/:id/stop-terms', validateOwnership, extraction.stopTermsUpdate) // Delete a specific stop term file. -router.delete('/:id/stop-terms/:filename', extraction.stopTermDelete) +router.delete( + '/:id/stop-terms/:filename', + validateOwnership, + extraction.stopTermDelete +) // Save oss params. -router.post('/:id/oss-save-params', extraction.ossSaveParams) +router.post('/:id/oss-save-params', validateOwnership, extraction.ossSaveParams) // Execute search by oss params. -router.put('/:id/oss-search', extraction.ossSearch) +router.put('/:id/oss-search', validateOwnership, extraction.ossSearch) // Confirm oss params. -router.put('/:id/oss-confirm-params', extraction.ossConfirmParams) +router.put( + '/:id/oss-confirm-params', + validateOwnership, + extraction.ossConfirmParams +) // Begin extraction. -router.put('/:id/begin', extraction.begin) +router.put('/:id/begin', validateOwnership, extraction.begin) // Duplicate extraction. -router.post('/:id/duplicate', extraction.duplicate) +router.post('/:id/duplicate', validateOwnership, extraction.duplicate) // Export term candidates. -router.get('/:id/term-candidates-export', extraction.termCandidatesExport) - -// List all finished extractions for current user. -// TODO Is this enpoint even necessarry? -router.get('/finished-for-user', extraction.listFinishedForUser) +router.get( + '/:id/term-candidates-export', + validateOwnership, + extraction.termCandidatesExport +) // List term candidates for specific extraction. -router.get('/:id/term-candidates', extraction.listTermCandidates) +router.get( + '/:id/term-candidates', + validateOwnership, + extraction.listTermCandidates +) module.exports = router diff --git a/express/routes/api/v1/index.js b/express/routes/api/v1/index.js index b2be38b..b54098e 100644 --- a/express/routes/api/v1/index.js +++ b/express/routes/api/v1/index.js @@ -7,7 +7,6 @@ const dictionariesRouter = require('./dictionaries') const portalsRouter = require('./portals') const searchRouter = require('./search') const consultancyRouter = require('./consultancy') -const demoPaginacijaRouter = require('./demo-paginacija') const extractionRouter = require('./extraction') router.use('/comments', commentsRouter) @@ -18,7 +17,6 @@ router.use('/dictionaries', dictionariesRouter) router.use('/portals', portalsRouter) router.use('/search', searchRouter) router.use('/consultancy', consultancyRouter) -router.use('/demo-paginacija', demoPaginacijaRouter) router.use('/extraction', extractionRouter) module.exports = router diff --git a/express/routes/api/v1/portals.js b/express/routes/api/v1/portals.js index 1a137e4..01e0798 100644 --- a/express/routes/api/v1/portals.js +++ b/express/routes/api/v1/portals.js @@ -1,4 +1,5 @@ const router = require('express-promise-router')() +const user = require('../../../middleware/user') const { create, @@ -10,6 +11,9 @@ const { deleteLinkedDictionary } = require('../../../controllers/api/v1/portals') +// All further routes are only available to portal admin. +router.use(user.isAuthenticated, user.isPortalAdmin) + // Create new portal connection router.post('/createPortal', create) diff --git a/express/routes/api/v1/search.js b/express/routes/api/v1/search.js index 6bce9a6..0b84143 100644 --- a/express/routes/api/v1/search.js +++ b/express/routes/api/v1/search.js @@ -5,11 +5,11 @@ const user = require('../../../middleware/user') // Return search results for main search. router.get('/main', search.listMainEntries) -// TODO This route assumes existance of user. Make sure to pre check it. // Return search results for editor search. router.get( '/editor/:dictionaryId', - user.isDictionaryEditor, + user.isAuthenticated, + user.canContentEdit, search.listEditorEntries ) diff --git a/express/routes/api/v1/system.js b/express/routes/api/v1/system.js index 0288fd3..026bb81 100644 --- a/express/routes/api/v1/system.js +++ b/express/routes/api/v1/system.js @@ -4,12 +4,18 @@ const { listDictionaries, syncDictionary } = require('../../../controllers/api/v1/inter_instance_sync') +const user = require('../../../middleware/user') // Receive Content Security Policy violation reports. router.post('/csp-reports', system.handleCspReports) // Trigger synchronization with Eurotermbank. -router.get('/eurotermbank-sync-push', system.syncWithEurotermbank) +router.get( + '/eurotermbank-sync-push', + user.isAuthenticated, + user.isPortalAdmin, + system.syncWithEurotermbank +) // List dictionaries (one page). router.get('/inter-instance-sync/dictionaries', listDictionaries) diff --git a/express/routes/api/v1/users.js b/express/routes/api/v1/users.js index 674d8e9..bfd447a 100644 --- a/express/routes/api/v1/users.js +++ b/express/routes/api/v1/users.js @@ -1,6 +1,7 @@ const router = require('express-promise-router')() const user = require('../../../controllers/users') const users = require('../../../controllers/api/v1/users') +const userAuth = require('../../../middleware/user') // Validate and create a new user. router.post('/register', user.register) @@ -8,12 +9,38 @@ router.post('/register', user.register) // Validate and log the user in. router.post('/login', user.login) -router.get('/addUser', user.findByUsernameOrEmail) +// Supply user with the token to set a new password. +router.post('/reset-password-init', user.generateResetPasswordToken) -router.get('/listAllUsers', users.listUsers) +// Validate new passwords, change if successful and sign the user in. +router.post('/reset-password-submit', user.changePassword) +// Find user by username or email in dictionary or admin console. +router.get( + '/addUser', + userAuth.isAuthenticated, + userAuth.canAdministrateDictionary, + user.findByUsernameOrEmail +) + +// List all users in the admin console. +router.get( + '/listAllUsers', + userAuth.isAuthenticated, + userAuth.isPortalAdmin, + users.listUsers +) + +// Update hits per page setting of the requesting user. router.post('/hitsPerPage', users.updateHitsPerPage) -router.post('/nameAndSurname', users.updateFristNameAndSurname) +// Update first name, last name and email of the requesting user. +router.post('/basic-data', users.updateBasicData) + +// Update password of the requesting user. +router.post('/password', users.updatePassword) + +// Delete user profile of the requesting user. +router.delete('/current', users.deleteCurrent) module.exports = router diff --git a/express/routes/consultancy.js b/express/routes/consultancy.js index a408156..7d850a2 100644 --- a/express/routes/consultancy.js +++ b/express/routes/consultancy.js @@ -1,5 +1,6 @@ const router = require('express-promise-router')() +const user = require('../middleware/user') const { consultancy, consultancyAdmin } = require('../controllers/consultancy') router.get('/', consultancy.index) @@ -12,23 +13,51 @@ router.get('/vprasanje/:id', consultancy.specificQuestion) router.use((req, res, next) => { if (req.isAuthenticated()) return next() - res.redirect('/') + res.redirect(303, '/') }) -router.get('/vprasanje/admin/novo', consultancyAdmin.new) +router.get( + '/vprasanje/admin/novo', + user.canAdministrateConsultancy, + consultancyAdmin.new +) -router.get('/vprasanje/admin/svetovalci', consultancyAdmin.users) +router.get( + '/vprasanje/admin/svetovalci', + user.canAdministrateConsultancy, + consultancyAdmin.users +) -router.get('/vprasanje/admin/zavrnjeno', consultancyAdmin.rejected) +router.get( + '/vprasanje/admin/zavrnjeno', + user.canAdministrateConsultancy, + consultancyAdmin.rejected +) -router.get('/vprasanje/admin/objavljeno', consultancyAdmin.published) +router.get( + '/vprasanje/admin/objavljeno', + user.canConsult, + consultancyAdmin.published +) -router.get('/vprasanje/admin/pripravljeno', consultancyAdmin.prepared) +router.get( + '/vprasanje/admin/pripravljeno', + user.canAdministrateConsultancy, + consultancyAdmin.prepared +) -router.get('/vprasanje/admin/v-delu', consultancyAdmin.inProgress) +router.get( + '/vprasanje/admin/v-delu', + user.canConsult, + consultancyAdmin.inProgress +) -router.get('/vprasanje/admin/statistika', consultancyAdmin.statistics) +// router.get('/vprasanje/admin/statistika', consultancyAdmin.statistics) -router.get('/vprasanje/admin/urejanje/:id', consultancyAdmin.edit) +router.get( + '/vprasanje/admin/urejanje/:id', + user.canConsultEntry, + consultancyAdmin.edit +) module.exports = router diff --git a/express/routes/dictionaries.js b/express/routes/dictionaries.js index d517a1a..ee16512 100644 --- a/express/routes/dictionaries.js +++ b/express/routes/dictionaries.js @@ -13,7 +13,7 @@ router.get('/:dictionaryId/o-slovarju', dictionary.dictionaryDetails) // All further routes are only available to authenticated users. router.use((req, res, next) => { if (req.isAuthenticated()) return next() - res.redirect(req.baseUrl) + res.redirect(303, req.baseUrl) }) // Show a form to create a new dictionary. @@ -92,13 +92,6 @@ router.get( dictionary.showContent ) -// Validate and update dictionary content entries. -// router.post( -// '/:dictionaryId/vsebina', -// user.isDictionaryEditor, -// dictionary.updateContent -// ) - // Show a form to export a dictionary. router.get( '/:dictionaryId/izvoz', diff --git a/express/routes/extraction-poc.js b/express/routes/extraction-poc.js deleted file mode 100644 index b32b52c..0000000 --- a/express/routes/extraction-poc.js +++ /dev/null @@ -1,27 +0,0 @@ -const router = require('express-promise-router')() -const extraction = require('../controllers/extraction-poc') - -// TODO In final version test authentication and authorization. - -// List all extractions. -router.get('/', extraction.list) - -// Create new extraction. -router.post('/', extraction.create) - -// Edit extraction. -router.get('/:id', extraction.edit) - -// Update extraction (own documents). -router.post('/:id', extraction.updateOwn) - -// Edit documents. -router.get('/:id/besedila', extraction.docsEdit) - -// Edit stop terms. -router.get('/:id/stop-termini', extraction.stopTermsEdit) - -// List term candidates for extraction and display export interface. -router.get('/:id/kandidati', extraction.listTermCandidates) - -module.exports = router diff --git a/express/routes/extraction.js b/express/routes/extraction.js index 36b708f..e8657e8 100644 --- a/express/routes/extraction.js +++ b/express/routes/extraction.js @@ -1,6 +1,5 @@ const router = require('express-promise-router')() const extraction = require('../controllers/extraction') -const extractionPocRouter = require('./extraction-poc') // List all extractions. router.get('/', extraction.list) @@ -8,54 +7,33 @@ router.get('/', extraction.list) // All further routes are only available to authenticated users. router.use((req, res, next) => { if (req.isAuthenticated()) return next() - res.redirect(req.baseUrl) + res.redirect(303, req.baseUrl) }) // Create new extraction. router.post('/', extraction.create) -router.use('/poc', extractionPocRouter) - // Edit extraction. -router.get('/:id', extraction.edit) +router.get('/:id', extraction.validateOwnership, extraction.edit) // Update extraction (own documents). -router.post('/:id', extraction.updateOwn) +router.post('/:id', extraction.validateOwnership, extraction.updateOwn) // Edit documents. -router.get('/:id/besedila', extraction.docsEdit) +router.get('/:id/besedila', extraction.validateOwnership, extraction.docsEdit) // Edit stop terms. -router.get('/:id/stop-termini', extraction.stopTermsEdit) +router.get( + '/:id/stop-termini', + extraction.validateOwnership, + extraction.stopTermsEdit +) // List term candidates for extraction and display export interface. -router.get('/:id/kandidati', extraction.listTermCandidates) - -// // TODO: Ask Luka or Miro what to do with this page. -// router.get('/id_luscenja/kandidati', (req, res) => { -// res.render('pages/extraction/terms', { -// title: 'Terminološki kandidati' -// }) -// }) - -// // Show a form to export a dictionary -// router.get('/id_luscenja/dokumenti/besedila', (req, res) => { -// res.render('pages/extraction/docs-edit', { -// title: 'Besedila' -// }) -// }) - -// // Show a form to export a dictionary -// router.get('/id_luscenja/oss', (req, res) => { -// res.render('pages/extraction/edit-oss', { -// title: 'KAS + dokumenti' -// }) -// }) - -// router.get('/id_luscenja/dokumenti/termini', (req, res) => { -// res.render('pages/extraction/stop-terms-edit', { -// title: 'Termini' -// }) -// }) +router.get( + '/:id/kandidati', + extraction.validateOwnership, + extraction.listTermCandidates +) module.exports = router diff --git a/express/routes/index.js b/express/routes/index.js index ef8201a..d423da1 100644 --- a/express/routes/index.js +++ b/express/routes/index.js @@ -1,11 +1,7 @@ const router = require('express-promise-router')() const { isAuthenticated: isUserAuthenticated } = require('../middleware/user') -const db = require('../models/db') -const cache = require('../models/cache') -const { searchEngineClient } = require('../models/search-engine') const index = require('../controllers/index') const user = require('../controllers/users') -const demoPaginacija = require('../controllers/demo-paginacija') // TODO Some of these endpoints might need authorization protection. Review and clean up at a later point. @@ -18,54 +14,6 @@ router.get('/iskanje', index.search) // Show a list of search results. router.get('/termin/:entryId', index.entryDetails) -/* -router.get('/termin/id_termina', (req, res) => { - res.render('pages/search/result-detail') -}) -*/ - -router.get('/keys', (req, res) => { - res.render('pages/search/special-keys-demo-rm-this') -}) - -router.get('/demo', (req, res) => { - res.render('pages/admin') -}) - -router.get('/select', (req, res) => { - res.render('pages/search-root') -}) - -router.get('/dictionaries', (req, res) => { - res.render('pages/my-dictionaries-root') -}) - -router.get('/db-query-demo', async (req, res) => { - const queryResult = await db.query("SELECT 'yes' as does_it_work") - res.send(queryResult.rows[0]) -}) - -router.get('/test-seje', (req, res) => { - // req.session.before = { a: 1, b: 'yes' } - const { sessionID, session } = req - // req.session.after = { c: 2, d: 'no' } - res.send({ sessionID, session }) -}) - -router.get('/test-cacha', async (req, res, next) => { - const sRes = await cache.set('my key', 'also my val') - const gRes = await cache.get('my key') - res.send({ sRes, gRes }) -}) - -router.get('/test-iskalnika', async (req, res) => { - const response = await searchEngineClient.ping() - res.send(response) -}) - -// Stran z demo implementacijo paginirane vsebine. -router.get('/demo-paginacija', demoPaginacija.izrišiStran) - // Render help page router.get('/pomoc', (req, res) => { res.render('pages/help', { title: req.t('Pomoč') }) @@ -86,17 +34,9 @@ router.get('/slovarji/xml-schema', (req, res) => { res.download('public/documents/dictionary_schema.xsd') }) -// Render demo help pug page. -router.get('/pomoc-pug-demo', (req, res) => { - res.render('pages/help-pug-demo-frame', { title: 'Pomoč - pug demo' }) -}) - // Activate user account. router.get('/users/activate', user.activateAccount) -// Log the user out. -router.post('/users/logout', user.logout) - // Render privacy policy page router.get('/politika-zasebnosti', (req, res) => { res.render(`pages/privacy-policy_${req.language}`, { @@ -111,6 +51,15 @@ router.get('/pogoji-uporabe', (req, res) => { }) }) +// Form for user to reset their password. +router.get('/ponastavitev-gesla', index.resetPassword) + +// Confirm user email change. +router.get('/sprememba-elektronskega-naslova', index.changeEmail) + +// Change user locale. +router.get('/spremeni-jezik/:languageCode', index.changeUserLanguage) + router.get('/moj-racun', isUserAuthenticated, index.myProfile) router.get('/izbrisi-racun', isUserAuthenticated, index.deleteProfile) @@ -119,9 +68,7 @@ router.get('/spremeni-geslo', isUserAuthenticated, index.changePassword) router.get('/nastavitve-racuna', isUserAuthenticated, index.userSettings) -router.get('/ponastavitev-gesla', index.resetPassword) - -// Change user locale. -router.get('/spremeni-jezik/:languageCode', index.changeUserLanguage) +// Log the user out. +router.post('/users/logout', isUserAuthenticated, user.logout) module.exports = router diff --git a/express/utils/index.js b/express/utils/index.js index c9a1e92..306774d 100644 --- a/express/utils/index.js +++ b/express/utils/index.js @@ -7,4 +7,6 @@ utils.pipe = (...fns) => (initialVal) => fns.reduce((val, fn) => fn(val), initia utils.composeAsync = (...fns) => input => fns.reduceRight((chain, func) => chain.then(func), Promise.resolve(input)); utils.pipeAsync = (...fns) => input => fns.reduce((chain, func) => chain.then(func), Promise.resolve(input)); +utils.capitalize = string => string[0].toUpperCase() + string.slice(1) + module.exports = utils diff --git a/express/views/common/side-menu-mixin-consultancy.pug b/express/views/common/side-menu-mixin-consultancy.pug index 204da0b..1351b35 100644 --- a/express/views/common/side-menu-mixin-consultancy.pug +++ b/express/views/common/side-menu-mixin-consultancy.pug @@ -7,7 +7,7 @@ mixin sideNavigation(metaData) src="/images/burger-menu-button-icon.svg" alt="Meni" ) - span#nav-title.nav-title= 'Svetovalnica' + span#nav-title.nav-title= t('Svetovalnica') #mobile-right-holder nav ul.admin-nav-content.slidable.z-20.scroller-style diff --git a/express/views/components/login-signup/login-signup.pug b/express/views/components/login-signup/login-signup.pug index 46ab42e..2c41894 100644 --- a/express/views/components/login-signup/login-signup.pug +++ b/express/views/components/login-signup/login-signup.pug @@ -70,7 +70,7 @@ type="checkbox" ) label.ps-2.align-middle.rememberloginlabel.text-header-description-gray.pe-2( - for="login-remember-me" + for="login-remember" )= t('Zapomni si prijavo') .col-5.pt-2.d-flex-and-align-end.pe-0 a#forgotten-password.ms-auto.forgotten-password.gray1-text.no-text-decoration.align-middle( diff --git a/express/views/components/navigation/main-navigation-right-mixin.pug b/express/views/components/navigation/main-navigation-right-mixin.pug index 420e3c0..1364337 100644 --- a/express/views/components/navigation/main-navigation-right-mixin.pug +++ b/express/views/components/navigation/main-navigation-right-mixin.pug @@ -58,18 +58,18 @@ mixin main-navigation-right(languageToolbarPresent=false, menuTextPresent=false, a.dropdown-item.ps-0.pb-2.navigation-text-color( href="/nastavitve-racuna" ) - .row - .col-3 - img(src="/images/cog.svg" alt="") - .col-9 + .d-flex.flex-row + .d-flex.flex-shrink-1.flex-grow-0 + img.pe-2(src="/images/cog.svg" alt="") + .d-flex.flex-grow-1 span.text-header-description-gray.d-block= t('Nastavitve') li.pb-0.mb-0.ps-3 form(action="/users/logout" method="post") - button.profile-menu-button - .row - .col-3 - img(src="/images/log-out.svg" alt="") - .col-9 + button.profile-menu-button.pb-2.pt-1 + .d-flex.flex-row + .d-flex.flex-shrink-1.flex-grow-0 + img.pe-2(src="/images/log-out.svg" alt="") + .d-flex.flex-grow-1 span.text-header-description-gray.d-block= t('Odjava') else button#login-static-backdrop-button.col-sm.nav-entry.no-border.no-bg( diff --git a/express/views/components/search-and-filter/advanced-search-modal-area-only.pug b/express/views/components/search-and-filter/advanced-search-modal-area-only.pug index 5414223..90ede8e 100644 --- a/express/views/components/search-and-filter/advanced-search-modal-area-only.pug +++ b/express/views/components/search-and-filter/advanced-search-modal-area-only.pug @@ -15,7 +15,7 @@ mixin consultancySM(adminFields=false) ) //- option.d-none(value="-1")= ' ' each domain in allPrimaryDomains - option(value=domain.id selected=domain.selected) #{ domain.nameSl } + option(value=domain.id selected=domain.selected)= domain.name span.advanced-input-label //- span.advanced-input-label Področje //- img#area-img(src="/images/chevron-down-darker.svg" alt="") diff --git a/express/views/components/search-and-filter/advanced-search-modal.pug b/express/views/components/search-and-filter/advanced-search-modal.pug index cc06272..89b6ce3 100644 --- a/express/views/components/search-and-filter/advanced-search-modal.pug +++ b/express/views/components/search-and-filter/advanced-search-modal.pug @@ -23,7 +23,7 @@ //- input#src-lang-input.box-sizing-content.bg-transparent select#src-lang-select.select-src-lang-field(multiple="multiple") each language in sourceLanguages - option(value=language.id selected=language.selected) #{ language.nameSl } + option(value=language.id selected=language.selected)= language.name span.advanced-input-label //- span.advanced-input-label Jezik iskanja //- img#src-lang-img(src="/images/chevron-down-darker.svg" alt="") @@ -35,7 +35,7 @@ //- input#dest-lang-input.box-sizing-content.bg-transparent select#dest-lang-select.select-dest-lang-field(multiple="multiple") each language in targetLanguages - option(value=language.id selected=language.selected) #{ language.nameSl } + option(value=language.id selected=language.selected)= language.name span.advanced-input-label //- span.advanced-input-label Ciljni jezik //- img#dest-lang-img(src="/images/chevron-down-darker.svg" alt="") @@ -62,7 +62,7 @@ multiple="multiple" ) each domain in allPrimaryDomains - option(value=domain.id selected=domain.selected) #{ domain.nameSl } + option(value=domain.id selected=domain.selected)= domain.name span.advanced-input-label //- span.advanced-input-label Področje //- img#area-img(src="/images/chevron-down-darker.svg" alt="") @@ -78,7 +78,7 @@ multiple="multiple" ) each dictionary in allDictionaryNames - option(value=dictionary.id selected=dictionary.selected) #{ dictionary.nameSl } + option(value=dictionary.id selected=dictionary.selected)= dictionary.name span.advanced-input-label //- span.advanced-input-label Slovar //- img#dict-img(src="/images/chevron-down-darker.svg" alt="") @@ -91,7 +91,7 @@ //- input#source-input.box-sizing-content.bg-transparent select#src-select.select-source-field(multiple="multiple") each portal in portals - option(value=portal.code selected=portal.selected) #{ portal.name } + option(value=portal.code selected=portal.selected)= portal.name span.advanced-input-label //- span.advanced-input-label Vir //- img#source-img(src="/images/chevron-down-darker.svg" alt="") diff --git a/express/views/components/search-and-filter/result-item.pug b/express/views/components/search-and-filter/result-item.pug index 6b65a94..e07f412 100644 --- a/express/views/components/search-and-filter/result-item.pug +++ b/express/views/components/search-and-filter/result-item.pug @@ -2,7 +2,7 @@ mixin result-item(headword, entry, isLast=false) - const foreignEntries = entry.foreignEntries ? entry.foreignEntries : [] const synonyms = entry.synonyms?entry.synonyms:[] - const domain = entry.primaryDomain&&entry.primaryDomain.nameSl?entry.primaryDomain.nameSl:"" + const domain = entry.primaryDomain?entry.primaryDomain[`name${capitalize(determinedLanguage)}`]:"" const slovenianDefinitionExists = !!entry.definition let foreignDefinitionExists = !!entry.foreignEntries && entry.foreignEntries.length > 0 let checkAllForeignDefinitions = false @@ -92,7 +92,7 @@ mixin result-item(headword, entry, isLast=false) .results-sources.d-flex.flex-shrink-1.justify-content-end.align-items-center .results-dict.d-flex.justify-content-end.align-items-center.me-2.px-2( data-bs-toggle="tooltip" - title=`${entry.dictionary.nameSl}` + title=`${entry.dictionary[`name${capitalize(determinedLanguage)}`]}` ) #{ entry.dictionary.nameSlShort } .results-portal.d-flex.justify-content-end.align-items-center.te( data-bs-toggle="tooltip" diff --git a/express/views/components/search-and-filter/search-filter-modal.pug b/express/views/components/search-and-filter/search-filter-modal.pug index d67580a..07fbf4e 100644 --- a/express/views/components/search-and-filter/search-filter-modal.pug +++ b/express/views/components/search-and-filter/search-filter-modal.pug @@ -22,7 +22,7 @@ section.filter-modal-root span.d-flex.flex-grow-1.align-content-center a#ccf.clear-filter-modal-section(href="#") img(src="/images/square-minus.svg" alt="") - span.ps-2 POČISTI FILTRE + span.ps-2= t('POČISTI FILTRE') .modal-body.pb-2 ul#modal-list.nav-modal-section-content.mb-0.pb-0 // Added dynamically... @@ -31,8 +31,6 @@ section.filter-modal-root button.btn.btn-light.border.border-dark.close( type="button" data-bs-dismiss="modal" - ) - | Zapri + )= t('Zapri') .justify-content-end - button#sf-modal-button.btn.btn-primary.use(type="button") - | Uporabi + button#sf-modal-button.btn.btn-primary.use(type="button")= t('Uporabi') diff --git a/express/views/email/dictionary-status-change.pug b/express/views/email/dictionary-status-change.pug index f28ed98..5790a2a 100644 --- a/express/views/email/dictionary-status-change.pug +++ b/express/views/email/dictionary-status-change.pug @@ -1,12 +1,12 @@ p Pozdravljeni, case type when 'status' - p oseba: #[b #{ changerEmail }] je spremenila stanje vašega slovarja: #[b #{ nameSl }]. + p oseba: #[b #{ changerEmail }] je spremenila stanje vašega slovarja: #[b #{ name }]. when 'unpublish' - p oseba: #[b #{ changerEmail }] je spremenila status slovarja #[b #{ nameSl }], ki ni več objavljen. + p oseba: #[b #{ changerEmail }] je spremenila status slovarja #[b #{ name }], ki ni več objavljen. when 'delete' - p slovar: #[b #{ nameSl }] ima manjše število gesel kot je nastavljeno. + p slovar: #[b #{ name }] ima manjše število gesel kot je nastavljeno. when 'publish-approval' - p slovar #[b #{ nameSl }] je bil dan v stanje preverjanja. Prosimo, da ga odprete/zaprete. + p slovar #[b #{ name }] je bil dan v stanje preverjanja. Prosimo, da ga odprete/zaprete. when 'publish-no-approval' - p slovar #[b #{ nameSl }] je bil objavljen. + p slovar #[b #{ name }] je bil objavljen. diff --git a/express/views/email/user-change-email-success_en.pug b/express/views/email/user-change-email-success_en.pug new file mode 100644 index 0000000..6a3c36f --- /dev/null +++ b/express/views/email/user-change-email-success_en.pug @@ -0,0 +1,2 @@ +p Dear #{ username }, +p you have successfully changed the e-mail address used for your Slovenian Terminology Portal user account. diff --git a/express/views/email/user-change-email-success_sl.pug b/express/views/email/user-change-email-success_sl.pug new file mode 100644 index 0000000..4e0d47a --- /dev/null +++ b/express/views/email/user-change-email-success_sl.pug @@ -0,0 +1,2 @@ +p Pozdravljeni #{ username }, +p to sporočilo ste prejeli, ker ste na Slovenskem terminološkem portalu uspešno spremenili elektronski naslov. diff --git a/express/views/email/user-change-email-token_en.pug b/express/views/email/user-change-email-token_en.pug new file mode 100644 index 0000000..f7472cc --- /dev/null +++ b/express/views/email/user-change-email-token_en.pug @@ -0,0 +1,4 @@ +p Dear #{ username }, +p you have received this message because you want to change the e-mail address used for your Slovenian Terminology Portal account. +p To change the e-mail address, please confirm that this is your new e-mail address. The link will be valid for 7 days. +a(href=changeEmailLink) I confirm my e-mail address diff --git a/express/views/email/user-change-email-token_sl.pug b/express/views/email/user-change-email-token_sl.pug new file mode 100644 index 0000000..104e100 --- /dev/null +++ b/express/views/email/user-change-email-token_sl.pug @@ -0,0 +1,4 @@ +p Pozdravljeni #{ username }, +p to sporočilo ste prejeli, ker na Slovenskem terminološkem portalu želite spremeniti elektronski naslov. +p Za spremembo naslova potrebujete le še potrditi, da je to res vaš novi elektronski naslov. Povezava je veljavna 7 dni. +a(href=changeEmailLink) Potrjujem svoj naslov diff --git a/express/views/email/user-change-password_en.pug b/express/views/email/user-change-password_en.pug new file mode 100644 index 0000000..c741906 --- /dev/null +++ b/express/views/email/user-change-password_en.pug @@ -0,0 +1,2 @@ +p Dear #{ username }, +p you have changed the password for your Slovenian Terminology Portal user account. diff --git a/express/views/email/user-change-password_sl.pug b/express/views/email/user-change-password_sl.pug new file mode 100644 index 0000000..f1ada0a --- /dev/null +++ b/express/views/email/user-change-password_sl.pug @@ -0,0 +1,2 @@ +p Pozdravljeni #{ username }, +p to sporočilo ste prejeli, ker ste na Slovenskem terminološkem portalu spremenili geslo. diff --git a/express/views/email/user-reset-password-success_en.pug b/express/views/email/user-reset-password-success_en.pug new file mode 100644 index 0000000..f53294e --- /dev/null +++ b/express/views/email/user-reset-password-success_en.pug @@ -0,0 +1,2 @@ +p Dear #{ username }, +p you have successfully reset the password for your Slovenian Terminology Portal user account. diff --git a/express/views/email/user-reset-password-success_sl.pug b/express/views/email/user-reset-password-success_sl.pug new file mode 100644 index 0000000..58e8ef5 --- /dev/null +++ b/express/views/email/user-reset-password-success_sl.pug @@ -0,0 +1,2 @@ +p Pozdravljeni #{ username }, +p to sporočilo ste prejeli, ker ste na Slovenskem terminološkem portalu uspešno ponastavili geslo. diff --git a/express/views/email/user-reset-password-token_en.pug b/express/views/email/user-reset-password-token_en.pug new file mode 100644 index 0000000..91ade9a --- /dev/null +++ b/express/views/email/user-reset-password-token_en.pug @@ -0,0 +1,4 @@ +p Dear #{ username }, +p you have received this message because you want to change the password for the Slovenian Terminology Portal user account connected to this e-mail address. +p To reset the password, click the link below. The link will be valid for 1 day. +a(href=resetPasswordLink) Password reset diff --git a/express/views/email/user-reset-password-token_sl.pug b/express/views/email/user-reset-password-token_sl.pug new file mode 100644 index 0000000..f4f4100 --- /dev/null +++ b/express/views/email/user-reset-password-token_sl.pug @@ -0,0 +1,4 @@ +p Pozdravljeni #{ username }, +p to sporočilo ste prejeli, ker na Slovenskem terminološkem portalu želite ponastaviti geslo uporabniškega računa s tem elektronskim naslovom. +p Za ponastavitev sledite spodnji povezavi. Povezava je veljavna 1 dan. +a(href=resetPasswordLink) Ponastavitev gesla diff --git a/express/views/extraction-poc/docs-edit.pug b/express/views/extraction-poc/docs-edit.pug deleted file mode 100644 index 19e999d..0000000 --- a/express/views/extraction-poc/docs-edit.pug +++ /dev/null @@ -1,29 +0,0 @@ -//- TODO MARK FOR DELETION - -h1 Dokumenti za luščenje #{ id } - -h2 Seznam datotek -ul#files-list - each document in extractionDocuments - li DATOTEKA - Ime: #[span.filename= document.filename], velikost: #{ document.size }, datum: #{ new Date(document.timeModified).toLocaleDateString('sl-SL') } #[a.delete-file(href="#") BRIŠI] - -h2 Dodaj nov(e) datoteke(e) -form#upload-files(method="post" enctype="multipart/form-data") - input(type="hidden" name="extractionId" value=id) - label Datoteka(e): - input( - type="file" - name="extractionFile" - accept=".txt, .doc, .docx, .pdf" - multiple - ) - -a(href=`../${id}`) Nazaj -p Obestila: -ul#messages - -script( - nonce=cspNonce - src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" -) -script(nonce=cspNonce src="/javascripts/extraction-poc-files-edit.js") diff --git a/express/views/extraction-poc/edit-oss.pug b/express/views/extraction-poc/edit-oss.pug deleted file mode 100644 index af52890..0000000 --- a/express/views/extraction-poc/edit-oss.pug +++ /dev/null @@ -1,128 +0,0 @@ -h1 Luščenje #{ id } - uredi - -form(method="post") - div - label Ime: - input(name="name" value=extraction.name) - div - label Glavno področje: - select(name="domain") - option - each domain in allPrimaryDomains - option(value=domain.id selected=domain.id === extraction.domainId)= domain.nameSl - div - label Vrsta dokumenta: - select.pick-multiple(name="documentType" multiple) - option - option(value=101 selected=extraction.documentType.includes(101)) Izvirni znanstveni članek - option(value=102 selected=extraction.documentType.includes(102)) Pregledni znanstveni članek - option(value=103 selected=extraction.documentType.includes(103)) Kratki znanstveni prispevek - option(value=104 selected=extraction.documentType.includes(104)) Strokovni članek - option(value=105 selected=extraction.documentType.includes(105)) Poljudni članek - option(value=106 selected=extraction.documentType.includes(106)) Objavljeni znanstveni prispevek na konferenci (vabljeno predavanje) - option(value=107 selected=extraction.documentType.includes(107)) Objavljeni strokovni prispevek na konferenci (vabljeno predavanje) - option(value=108 selected=extraction.documentType.includes(108)) Objavljeni znanstveni prispevek na konferenci - option(value=109 selected=extraction.documentType.includes(109)) Objavljeni strokovni prispevek na konferenci - option(value=110 selected=extraction.documentType.includes(110)) Objavljeni povzetek znanstvenega prispevka na konferenci (vabljeno predavanje) - option(value=112 selected=extraction.documentType.includes(112)) Objavljeni povzetek znanstvenega prispevka na konferenci - option(value=113 selected=extraction.documentType.includes(113)) Objavljeni povzetek strokovnega prispevka na konferenci - option(value=116 selected=extraction.documentType.includes(116)) Samostojni znanstveni sestavek ali poglavje v monografski publikaciji - option(value=117 selected=extraction.documentType.includes(117)) Samostojni strokovni sestavek ali poglavje v monografski publikaciji - option(value=118 selected=extraction.documentType.includes(118)) Geslo – sestavek v enciklopediji, leksikonu, slovarju... - option(value=119 selected=extraction.documentType.includes(119)) Recenzija, prikaz knjige, kritika - option(value=120 selected=extraction.documentType.includes(120)) Predgovor, spremna beseda - option(value=121 selected=extraction.documentType.includes(121)) Polemika, diskusijski prispevek - option(value=122 selected=extraction.documentType.includes(122)) Intervju - option(value=123 selected=extraction.documentType.includes(123)) Umetniški sestavek - option(value=124 selected=extraction.documentType.includes(124)) Bibliografija, kazalo ipd. - option(value=125 selected=extraction.documentType.includes(125)) Drugi članki ali sestavki - option(value=201 selected=extraction.documentType.includes(201)) Znanstvena monografija - option(value=202 selected=extraction.documentType.includes(202)) Strokovna monografija - option(value=203 selected=extraction.documentType.includes(203)) Univerzitetni, visokošolski ali višješolski učbenik z recenzijo - option(value=204 selected=extraction.documentType.includes(204)) Srednješolski, osnovnošolski ali drugi učbenik z recenzijo - option(value=205 selected=extraction.documentType.includes(205)) Video in druga učna gradiva - option(value=206 selected=extraction.documentType.includes(206)) Enciklopedija, slovar, leksikon, priročnik, atlas, zemljevid - option(value=207 selected=extraction.documentType.includes(207)) Bibliografija - option(value=208 selected=extraction.documentType.includes(208)) Doktorska disertacija - option(value=209 selected=extraction.documentType.includes(209)) Magistrsko delo - option(value=210 selected=extraction.documentType.includes(210)) Specialistično delo - option(value=211 selected=extraction.documentType.includes(211)) Diplomsko delo - option(value=212 selected=extraction.documentType.includes(212)) Končno poročilo o rezultatih raziskav - option(value=213 selected=extraction.documentType.includes(213)) Elaborat, predštudija, študija - option(value=214 selected=extraction.documentType.includes(214)) Projektna dokumentacija (idejni projekt, izvedbeni projekt) - option(value=215 selected=extraction.documentType.includes(215)) Izvedensko mnenje, arbitražna odločba - option(value=216 selected=extraction.documentType.includes(216)) Umetniško delo - option(value=217 selected=extraction.documentType.includes(217)) Katalog razstave - option(value=218 selected=extraction.documentType.includes(218)) Raziskovalni ali dokumentarni film, zvočni ali video posnetek - option(value=219 selected=extraction.documentType.includes(219)) Radijska ali televizijska oddaja - option(value=220 selected=extraction.documentType.includes(220)) Raziskovalni podatki - option(value=221 selected=extraction.documentType.includes(221)) Programska oprema - option(value=222 selected=extraction.documentType.includes(222)) Nova sorta - option(value=223 selected=extraction.documentType.includes(223)) Patentna prijava - option(value=224 selected=extraction.documentType.includes(224)) Patent - option(value=225 selected=extraction.documentType.includes(225)) Druge monografije in druga zaključena dela - option(value=227 selected=extraction.documentType.includes(227)) Znanstveni terminološki slovar, enciklopedija ali tematski leksikon - option(value=230 selected=extraction.documentType.includes(230)) Zbornik strokovnih ali nerecenziranih znanstvenih prispevkov na konferenci - option(value=231 selected=extraction.documentType.includes(231)) Zbornik recenziranih znanstvenih prispevkov na mednarodni ali tuji konferenci - option(value=232 selected=extraction.documentType.includes(232)) Zbornik recenziranih znanstvenih prispevkov na domači konferenci - option(value=310 selected=extraction.documentType.includes(310)) Umetniška poustvaritev - option(value=311 selected=extraction.documentType.includes(311)) Radijski ali TV dogodek - option(value=312 selected=extraction.documentType.includes(312)) Razstava - option(value=313 selected=extraction.documentType.includes(313)) Organiziranje znanstvenih in strokovnih sestankov - option(value=314 selected=extraction.documentType.includes(314)) Predavanje na tuji univerzi - option(value=315 selected=extraction.documentType.includes(315)) Prispevek na konferenci brez natisa - option(value=316 selected=extraction.documentType.includes(316)) Vabljeno predavanje na konferenci brez natisa - option(value=320 selected=extraction.documentType.includes(320)) Druga dela - option(value=325 selected=extraction.documentType.includes(325)) Druga izvedena dela - div - label Leto: - select.enter-multiple(name="year" multiple style="width: 500px") - each year in extraction.year - option(value=year selected)= year - div - label Ključne besede: - select.enter-multiple(name="keywords" multiple style="width: 500px") - each keyword in extraction.keywords - option(value=keyword selected)= keyword - -h2 Stop termini: -ul - each file in stopTermsFiles - li DATOTEKA - Ime: #[span.filename= file.filename], velikost: #{ file.size }, datum: #{ new Date(file.timeModified).toLocaleDateString('sl-SL') } -a#edit-stop-terms(href=`${id}/stop-termini`) Uredi - -br -br -button#search-btn Najdi -#search-result - -br -br -a(href="../poc") Nazaj - -p Obestila: -ul#messages - -link( - href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" - rel="stylesheet" - integrity="sha256-zaSoHBhwFdle0scfGEFUCwggPN7F+ip9XRglo8IWb4w=" - crossorigin="anonymous" -) -script( - nonce=cspNonce - src="https://code.jquery.com/jquery-3.6.0.slim.min.js" - integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" - crossorigin="anonymous" -) -script( - nonce=cspNonce - src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js" - integrity="sha256-9yRP/2EFlblE92vzCA10469Ctd0jT48HnmmMw5rJZrA=" - crossorigin="anonymous" -) -script( - nonce=cspNonce - src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" -) -script(nonce=cspNonce src="/javascripts/extraction-poc-oss-edit.js") diff --git a/express/views/extraction-poc/edit-own.pug b/express/views/extraction-poc/edit-own.pug deleted file mode 100644 index 8878293..0000000 --- a/express/views/extraction-poc/edit-own.pug +++ /dev/null @@ -1,20 +0,0 @@ -h1 Luščenje #{ id } - uredi - -form(method="post") - label Ime: - input(name="name" value=extraction.name) - button Potrdi - -h2 Lastni dokumenti: -ul - each document in extractionDocuments - li DATOTEKA - Ime: #[span.filename= document.filename], velikost: #{ document.size }, datum: #{ new Date(document.timeModified).toLocaleDateString('sl-SL') } -a(href=`${id}/besedila`) Uredi -h2 Stop termini: -ul - each file in stopTermsFiles - li DATOTEKA - Ime: #[span.filename= file.filename], velikost: #{ file.size }, datum: #{ new Date(file.timeModified).toLocaleDateString('sl-SL') } -a(href=`${id}/stop-termini`) Uredi - -br -a(href="../poc") Nazaj diff --git a/express/views/extraction-poc/list.pug b/express/views/extraction-poc/list.pug deleted file mode 100644 index 9a65ff2..0000000 --- a/express/views/extraction-poc/list.pug +++ /dev/null @@ -1,44 +0,0 @@ -h1 Luščenje - -form(method="post") - input(type="hidden" name="extractionType" value="own") - p Luščenje iz lastnih besedil - = ' ' - button Dodaj - -form(method="post") - input(type="hidden" name="extractionType" value="oss") - p Luščenje iz OSS besedil - = ' ' - button Dodaj - -h2 Seznam obstoječih luščenj -ul#extraction-list - each e in extractions - li(data-id=e.id) - | #{ e.name } (STATUS: #[span.extraction-status= e.status]) ( - if e.status === 'new' - a(href=`poc/${e.id}`) Uredi - if e.status !== 'in progress' - button.btn-delete Izbriši - if e.canBegin - button.btn-begin Začni - if e.status === 'finished' || e.status === 'failed' - button.btn-duplicate Podvoji - if e.status === 'finished' - a(href=`poc/${e.id}/kandidati`) Terminološki kandidati [#{ e.termCandidatesCount }] - if e.status === 'finished' && e.corpusId - a(href=`poc/korpus/${e.corpusId}` target="_blank") Uporabniški korpus - if e.status === 'finished' && e.ossParams - a( - href="https://www.clarin.si/ske/#dashboard?corpname=oss" - target="_blank" - ) Korpus KAS+ - | ) (ZAČETEK: #[span.extraction-time-started= e.timeStarted ? e.timeStarted.toLocaleString('sl-SI') : 'ni še bilo začeto'], - | KONEC: #[span.extraction-time-finished= e.timeFinished ? e.timeFinished.toLocaleString('sl-SI') : 'ni še bilo končano']) - -script( - nonce=cspNonce - src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" -) -script(nonce=cspNonce src="/javascripts/extraction-poc-list.js") diff --git a/express/views/extraction-poc/stop-terms-edit.pug b/express/views/extraction-poc/stop-terms-edit.pug deleted file mode 100644 index e9cd020..0000000 --- a/express/views/extraction-poc/stop-terms-edit.pug +++ /dev/null @@ -1,22 +0,0 @@ -h1 Stop termini za luščenje #{ id } - -h2 Seznam datotek -ul#files-list - each file in stopTermsFiles - li DATOTEKA - Ime: #[span.filename= file.filename], velikost: #{ file.size }, datum: #{ new Date(file.timeModified).toLocaleDateString('sl-SL') } #[a.delete-file(href="#") BRIŠI] - -h2 Dodaj nov(e) datoteke(e) -form#upload-files(method="post" enctype="multipart/form-data") - input(type="hidden" name="extractionId" value=id) - label Datoteka(e): - input(type="file" name="extractionFile" accept=".txt" multiple) - -a(href=`../${id}`) Nazaj -p Obestila: -ul#messages - -script( - nonce=cspNonce - src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" -) -script(nonce=cspNonce src="/javascripts/extraction-poc-files-edit.js") diff --git a/express/views/extraction-poc/term-candidates.pug b/express/views/extraction-poc/term-candidates.pug deleted file mode 100644 index a5fd380..0000000 --- a/express/views/extraction-poc/term-candidates.pug +++ /dev/null @@ -1,23 +0,0 @@ -h1 Terminološki kandidati - -include /utilities/pager -+pager - -ul#page-results - each termCandidate, index in firstPageOfTermCandidates - li [#{ index + 1 }] #{ JSON.stringify(termCandidate) } - -form(action=`/api/v1/extraction/${extractionId}/term-candidates-export`) - p Izvozi termine od številke - input(type="number" name="from") - p do številke - input(type="number" name="to") - button Izvozi - -script#variables-transport-script(nonce=cspNonce). - const termCandidates = !{ termCandidatesJson }.terminoloski_kandidati; - const hitsPerPage = #{ hitsPerPage }; - const numberOfAllPages = #{ numberOfAllPages }; - document.getElementById('variables-transport-script').remove(); - -script(nonce=cspNonce src="/javascripts/extraction-poc-term-candidates.js") diff --git a/express/views/layout.pug b/express/views/layout.pug index e6cb624..b085dc0 100644 --- a/express/views/layout.pug +++ b/express/views/layout.pug @@ -100,6 +100,13 @@ html(lang=language) crossorigin="anonymous" ) + if flashInfo.length + include /utilities/modal-response + +responseModal("flash-modal", t("Razumem"), "understand-btn", flashInfo[0]) + script(nonce=cspNonce). + const flashModal = new bootstrap.Modal(document.getElementById('flash-modal')); + flashModal.toggle(); + if inDevEnv script(nonce=cspNonce) window.inDevEnv = true script(nonce=cspNonce src="/javascripts/globals.js") diff --git a/express/views/pages/admin/areas.pug b/express/views/pages/admin/areas.pug index ffce32b..9a947df 100644 --- a/express/views/pages/admin/areas.pug +++ b/express/views/pages/admin/areas.pug @@ -70,14 +70,14 @@ block body #input-row.row .col-sm-3 .subject-name - label.input-name-txt= ('PODPODROČJE') + label.input-name-txt= t('PODPODROČJE') input#area-input.form-control(type="text") .col-sm-3 .subject-name - label.input-name-txt= ('ANGLEŠKI PREVOD') + label.input-name-txt= t('ANGLEŠKI PREVOD') input#translation-input.form-control(type="text") .col.d-flex.align-items-end.mt-2 - button#add-area.btn.btn-primary(type="submit" disabled)= ('Dodaj') + button#add-area.btn.btn-primary(type="submit" disabled)= t('Dodaj') include /common/footer include /utilities/modal-alert diff --git a/express/views/pages/admin/connections-list.pug b/express/views/pages/admin/connections-list.pug index de47f07..8cba3a6 100644 --- a/express/views/pages/admin/connections-list.pug +++ b/express/views/pages/admin/connections-list.pug @@ -43,7 +43,7 @@ block body .d-flex.align-content-center.mb-0.me-3.mt-2.mt-sm-0 a.btn.p-0.image-link( type="link" - href=`/admin/povezava/${link.id}/urejanje` + href=`/admin/povezave/${link.id}/urejanje` ) img(src="/images/u_edit-alt.svg" alt="Uredi") span.normal-gray.ms-1= t('Uredi') @@ -95,7 +95,7 @@ block body .d-flex.align-content-center.mb-0.me-3.mt-2.mt-sm-0 a.btn.p-0.image-link( type="link" - href=`/admin/povezava/${link.id}/urejanje` + href=`/admin/povezave/${link.id}/urejanje` ) img(src="/images/u_edit-alt.svg" alt="Uredi") span.normal-gray.ms-1= t('Uredi') diff --git a/express/views/pages/admin/dictionary-description.pug b/express/views/pages/admin/dictionary-description.pug index 924615f..a433b6a 100644 --- a/express/views/pages/admin/dictionary-description.pug +++ b/express/views/pages/admin/dictionary-description.pug @@ -12,8 +12,7 @@ block body +sideNavigation(d) include /utilities/dictionaries-main-panel-header - //- TODO I18n - - const c = { sideMenu: true, h2: dictionary.nameSl, h1: t('Osnovni podatki'), description: t('Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.'), helpLink: { linkHref: '/pomoc#help-edit-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-description' }] } + - const c = { sideMenu: true, h2: dictionary[`name${capitalize(determinedLanguage)}`], h1: t('Osnovni podatki'), description: t('Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.'), helpLink: { linkHref: '/pomoc#help-edit-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-description' }] } +mainHeader(c) include /utilities/dictionary-description-mixin +dictionary-description(dictionary.id) diff --git a/express/views/pages/admin/dictionary-structure.pug b/express/views/pages/admin/dictionary-structure.pug index 2956c17..cde2c40 100644 --- a/express/views/pages/admin/dictionary-structure.pug +++ b/express/views/pages/admin/dictionary-structure.pug @@ -12,8 +12,7 @@ block body +sideNavigation(d) include /utilities/dictionaries-main-panel-header - //- TODO I18n - - const c = { sideMenu: true, h2: dictionary.nameSl, h1: t('Struktura slovarskega sestavka'), description: t('V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.'), helpLink: { linkHref: '/pomoc#help-structure-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'form-dictionary-structure' }] } + - const c = { sideMenu: true, h2: dictionary[`name${capitalize(determinedLanguage)}`], h1: t('Struktura slovarskega sestavka'), description: t('V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.'), helpLink: { linkHref: '/pomoc#help-structure-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'form-dictionary-structure' }] } +mainHeader(c) .content-hold-prerequisites #offset-main.main-container diff --git a/express/views/pages/admin/dictionary-users.pug b/express/views/pages/admin/dictionary-users.pug index 1e257b7..95bab20 100644 --- a/express/views/pages/admin/dictionary-users.pug +++ b/express/views/pages/admin/dictionary-users.pug @@ -12,7 +12,7 @@ block body +sideNavigation(d) include /utilities/dictionaries-main-panel-header - - const c = { sideMenu: true, h2: dictionary.nameSl, h1: t('Uporabniki'), description: t('V tem razdelku lahko določite uporabniške vloge posameznega uporabnika in urejate njegove podatke.'), buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-dictionary-users' }] } + - const c = { sideMenu: true, h2: dictionary.name, h1: t('Uporabniki'), description: t('V tem razdelku lahko določite uporabniške vloge posameznega uporabnika in urejate njegove podatke.'), buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-dictionary-users' }] } +mainHeader(c) .content-hold-prerequisites #offset-main.main-container.mt-1 diff --git a/express/views/pages/admin/user-list.pug b/express/views/pages/admin/user-list.pug index 3a0d480..f81b004 100644 --- a/express/views/pages/admin/user-list.pug +++ b/express/views/pages/admin/user-list.pug @@ -36,7 +36,7 @@ block body tr td= result.userName td= result.email - td= result.status + td= t(`userStatus${result.status}`) td a.image-link( type="link" diff --git a/express/views/pages/consultancy/admin/edit.pug b/express/views/pages/consultancy/admin/edit.pug index 2fafccf..8845e87 100644 --- a/express/views/pages/consultancy/admin/edit.pug +++ b/express/views/pages/consultancy/admin/edit.pug @@ -79,7 +79,7 @@ block body option( value=domain.id selected=domain.id === entry.domainPrimaryId - )= domain.nameSl + )= domain.name #invalid-section.invalid-feedback-selection.hidden-section.mt-3 #{ t('Nimate izbranega področja CERIF.') } //- .col-sm.d-flex.align-items-center span.d-md-inline.d-block.smaller-gray-info.ms-xxl-3.ms-md-3 Področje, v katero sodi opisani terminološki problem diff --git a/express/views/pages/consultancy/ask.pug b/express/views/pages/consultancy/ask.pug index 0a60670..01729fd 100644 --- a/express/views/pages/consultancy/ask.pug +++ b/express/views/pages/consultancy/ask.pug @@ -96,7 +96,7 @@ block body ) option(value=-1) each domain in allPrimaryDomains - option(value=domain.id)= domain.nameSl + option(value=domain.id)= domain.name #invalid-section.invalid-feedback-selection.hidden-section.mt-3 #{ t('Nimate izbranega področja CERIF.') } .col-sm.d-flex.align-items-center span.d-md-inline.d-block.smaller-gray-info.ms-xxl-3.ms-md-3 #{ t('Področje, v katero sodi opisani terminološki problem.') } @@ -133,5 +133,8 @@ block body include /utilities/modal-response +responseModal("begin-response", t("Razumem"), "understand-btn", isOwnConsultancyEnabled?t("Hvala za poslano vprašanje, ki je bilo posredovano svetovalcem terminološkega portala. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji."):t("Hvala za poslano vprašanje, ki je bilo posredovano v Terminološko svetovalnico ZRC SAZU. Odgovor boste prejeli na e-naslov, ki ste ga navedli ob registraciji.")) else - p #{ t('Za zastavljanje terminoloških vprašanj morate biti prijavljeni.') } + .consultancy-container + .text-center.mt-5 + .pt-3 + p #{ t('Za zastavljanje terminoloških vprašanj morate biti prijavljeni.') } include /common/footer diff --git a/express/views/pages/demo-paginacija.pug b/express/views/pages/demo-paginacija.pug deleted file mode 100644 index 25a2450..0000000 --- a/express/views/pages/demo-paginacija.pug +++ /dev/null @@ -1,50 +0,0 @@ -//- TODO MARK FOR DELETION - -doctype html -html(lang="sl") - head - meta(charset="UTF-8") - meta(http-equiv="X-UA-Compatible" content="IE=edge") - meta(name="viewport" content="width=device-width, initial-scale=1.0") - title Paginacija - POC - body - h1 Demo konceptualna zasnova paginacije (muštr) - - //- Id krovnega elementa je v realnem primeru lahko poljuben, saj ga podaš funkciji initPagination (v demo-paginacija.js). - //- UPDATE: initPagination sedaj lahko sprejme tudi array idjev (tukaj: pagination-top in pagination-bottom) - //- Interna struktura mora ostati enaka (imena elementov, njihove klase, imena, vrednosti). - //- Lahko pa v gumbe dodaš slike itd., če je potrebno, ali dodatne klase na njih, če je potrebno zaradi oblikovanja. - //- Oblikovanja tu namenoma nisem dodajal, da je demo čim enostavnejši. Oblikovati ga morata vidva oz. prvi, ki bo to delal. - //- Če kaj ne bo jasno, me raje vprašaj. - #pagination-top - button.first-page(disabled) << - button.previous-page(disabled) < - form - input(name="page" value="1") - span= ' / ' - span.pages-total= numberOfAllPages - button.next-page(disabled=numberOfAllPages === 1) > - button.last-page(disabled=numberOfAllPages === 1) >> - - ul#page-results - each result in results - li Zanimiva vrednost: #[b= result.zanimivo]. Totalno nezanimivo: #{ result.nezanimivo1 } in #{ result.nezanimivo2 } - - #pagination-bottom - button.first-page(disabled) << - button.previous-page(disabled) < - form - input(name="page" value="1") - span= ' / ' - span.pages-total= numberOfAllPages - button.next-page(disabled=numberOfAllPages === 1) > - button.last-page(disabled=numberOfAllPages === 1) >> - - //- V realnem primeru: - //- Ne uvoziš axiosa, ker je že v layout.pug - //- Ne uvoziš skripte demo-paginacija.js, ker inicializacijsko kodo po njenem zgledu uporabiš v skripti, ki jo ta stan že uporablja. - script( - nonce=cspNonce - src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" - ) - script(nonce=cspNonce src="/javascripts/demo-paginacija.js") diff --git a/express/views/pages/dictionaries/description.pug b/express/views/pages/dictionaries/description.pug index 37fdac4..3f390dc 100644 --- a/express/views/pages/dictionaries/description.pug +++ b/express/views/pages/dictionaries/description.pug @@ -1,7 +1,7 @@ extends /layout block pageSpecificScipts - script(nonce=cspNonce src="/javascripts/dictionaries.js") + script(nonce=cspNonce src="/javascripts/admin.js") block body section#fixed-top-section @@ -12,8 +12,7 @@ block body - const d = { activeLvl1: 'attributes', activeLvl2: 'description' } +sideNavigation(d) include /utilities/dictionaries-main-panel-header - //- TODO I18n - - const c = { sideMenu: true, h2: dictionary.nameSl, h1: t('Osnovni podatki'), description: t('Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.'), helpLink: { linkHref: '/pomoc#help-edit-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'form-dictionary-description' }] } + - const c = { sideMenu: true, h2: dictionary[`name${capitalize(determinedLanguage)}`], h1: t('Osnovni podatki'), description: t('Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.'), helpLink: { linkHref: '/pomoc#help-edit-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-description' }] } +mainHeader(c) include /utilities/dictionary-description-mixin +dictionary-description diff --git a/express/views/pages/dictionaries/dictlist.pug b/express/views/pages/dictionaries/dictlist.pug index 13dd268..ce2e2ee 100644 --- a/express/views/pages/dictionaries/dictlist.pug +++ b/express/views/pages/dictionaries/dictlist.pug @@ -22,7 +22,7 @@ block body href="/" ) img.align-middle(src="/images/chevrons-left.svg") - span.ms-2.text-header-description-gray.fw-700 Nazaj + span.ms-2.text-header-description-gray.fw-700= t('Nazaj') .d-flex.justify-content-between.flex-md-nowrap.flex-wrap .header-container-left-side.d-flex .header-container-divider-left diff --git a/express/views/pages/dictionaries/list.pug b/express/views/pages/dictionaries/list.pug index f616616..383ff5e 100644 --- a/express/views/pages/dictionaries/list.pug +++ b/express/views/pages/dictionaries/list.pug @@ -27,7 +27,7 @@ block body class=index ? 'container-fluid dictionary-content mt-4 pb-2 pt-1' : 'container-fluid dictionary-content mt-3 pb-2 pt-1' ) //- TODO I18n - p.dictionary-title.mb-2.mt-2.ms-2= dictionary.nameSl + p.dictionary-title.mb-2.mt-2.ms-2= dictionary.name hr.me-2.mt-0.ms-2.mb-4 .container-fluid .d-sm-flex diff --git a/express/views/pages/dictionaries/new.pug b/express/views/pages/dictionaries/new.pug index 62859a4..edad5cb 100644 --- a/express/views/pages/dictionaries/new.pug +++ b/express/views/pages/dictionaries/new.pug @@ -11,7 +11,7 @@ block body include /common/side-menu-fake +sideNavigationFake include /utilities/dictionaries-main-panel-header - - const c = {sideMenu:false, h2: t('Urejanje'), h1: t('Nov slovar'), description: t('Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.'), helpLink: { linkHref: '/pomoc#help-editor', linkText: 'Več ...' }, buttons: [{ type: 'cancel', content: 'Prekliči', classAtr:"btn btn-secondary header-btn-secondary link-back" },{ type: 'button', content: t('Ustvari'), form: 'form-dictionary-new' }] } + - const c = {sideMenu:false, h2: t('Urejanje'), h1: t('Nov slovar'), description: t('Izpolnite polja in na kratko opišite vsebino terminološkega slovarja.'), helpLink: { linkHref: '/pomoc#help-editor', linkText: t('Več ...') }, buttons: [{ type: 'cancel', content: t('Prekliči'), classAtr:"btn btn-secondary header-btn-secondary link-back" },{ type: 'button', content: t('Ustvari'), form: 'form-dictionary-new' }] } +mainHeader(c) .content-hold-prerequisites #offset-main.container-fluid.mt-4.ps-sm-3 diff --git a/express/views/pages/dictionaries/structure.pug b/express/views/pages/dictionaries/structure.pug index 5ebd19c..a4fa2ed 100644 --- a/express/views/pages/dictionaries/structure.pug +++ b/express/views/pages/dictionaries/structure.pug @@ -12,7 +12,7 @@ block body +sideNavigation(d) include /utilities/dictionaries-main-panel-header - - const c = { sideMenu: true, h2: dictionary.nameSl, h1: t('Struktura slovarskega sestavka'), description: t('V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.'), helpLink: { linkHref: '/pomoc#help-structure-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'form-dictionary-structure' }] } + - const c = { sideMenu: true, h2: dictionary[`name${capitalize(determinedLanguage)}`], h1: t('Struktura slovarskega sestavka'), description: t('V tem razdelku lahko določite elemente slovarskega sestavka. Z izbiro elementov se vam prikazuje podoba slovarskega sestavka. Izbiro lahko tudi med urejanjem vsebine kadarkoli spremenite.'), helpLink: { linkHref: '/pomoc#help-structure-dict', linkText: t('Več ...') }, buttons: [{ type: 'disabled', content: t('Shrani'), form: 'form-dictionary-structure' }] } +mainHeader(c) .content-hold-prerequisites #offset-main.main-container diff --git a/express/views/pages/dictionaries/users.pug b/express/views/pages/dictionaries/users.pug index a465164..b20bca1 100644 --- a/express/views/pages/dictionaries/users.pug +++ b/express/views/pages/dictionaries/users.pug @@ -12,8 +12,7 @@ block body +sideNavigation(d) include /utilities/dictionaries-main-panel-header - //- TODO I18n - - const c = { sideMenu: true, h2: dictionary.nameSl, h1: t('Uporabniki'), description: t('Registrirani uporabniki lahko dobijo različne vloge na portalu. Kot administrator jim lahko dodelite tudi vlogo skrbnika slovarjev in/ali skrbnika svetovalnice.'), buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-dictionary-users' }] } + - const c = { sideMenu: true, h2: dictionary.name, h1: t('Uporabniki'), description: t('Registrirani uporabniki lahko dobijo različne vloge na portalu. Kot administrator jim lahko dodelite tudi vlogo skrbnika slovarjev in/ali skrbnika svetovalnice.'), buttons: [{ type: 'disabled', content: t('Shrani'), form: 'admin-dictionary-users' }] } +mainHeader(c) .content-hold-prerequisites #offset-main.main-container.mt-1 diff --git a/express/views/pages/extraction/edit-oss.pug b/express/views/pages/extraction/edit-oss.pug index 736c92a..9e284b8 100644 --- a/express/views/pages/extraction/edit-oss.pug +++ b/express/views/pages/extraction/edit-oss.pug @@ -40,7 +40,7 @@ block body option( value=domain.id selected=domain.id === extraction.domainId - )= domain.nameSl + )= domain.name .col-sm.d-flex.align-items-center span.d-md-inline.d-block.smaller-gray-info.ms-xxl-3.ms-md-3= t('Izberite področje iz seznama področij, da zmanjšate obseg besedil, iz katerih bo potekalo luščenje.') @@ -53,248 +53,12 @@ block body name="documentType" multiple ) - //- TODO i18n - Angleški šifrant, baza, itd.? option - option( - value=101 - selected=extraction.documentType.includes(101) - ) Izvirni znanstveni članek - option( - value=102 - selected=extraction.documentType.includes(102) - ) Pregledni znanstveni članek - option( - value=103 - selected=extraction.documentType.includes(103) - ) Kratki znanstveni prispevek - option( - value=104 - selected=extraction.documentType.includes(104) - ) Strokovni članek - option( - value=105 - selected=extraction.documentType.includes(105) - ) Poljudni članek - option( - value=106 - selected=extraction.documentType.includes(106) - ) Objavljeni znanstveni prispevek na konferenci (vabljeno predavanje) - option( - value=107 - selected=extraction.documentType.includes(107) - ) Objavljeni strokovni prispevek na konferenci (vabljeno predavanje) - option( - value=108 - selected=extraction.documentType.includes(108) - ) Objavljeni znanstveni prispevek na konferenci - option( - value=109 - selected=extraction.documentType.includes(109) - ) Objavljeni strokovni prispevek na konferenci - option( - value=110 - selected=extraction.documentType.includes(110) - ) Objavljeni povzetek znanstvenega prispevka na konferenci (vabljeno predavanje) - option( - value=112 - selected=extraction.documentType.includes(112) - ) Objavljeni povzetek znanstvenega prispevka na konferenci - option( - value=113 - selected=extraction.documentType.includes(113) - ) Objavljeni povzetek strokovnega prispevka na konferenci - option( - value=116 - selected=extraction.documentType.includes(116) - ) Samostojni znanstveni sestavek ali poglavje v monografski publikaciji - option( - value=117 - selected=extraction.documentType.includes(117) - ) Samostojni strokovni sestavek ali poglavje v monografski publikaciji - option( - value=118 - selected=extraction.documentType.includes(118) - ) Geslo – sestavek v enciklopediji, leksikonu, slovarju... - option( - value=119 - selected=extraction.documentType.includes(119) - ) Recenzija, prikaz knjige, kritika - option( - value=120 - selected=extraction.documentType.includes(120) - ) Predgovor, spremna beseda - option( - value=121 - selected=extraction.documentType.includes(121) - ) Polemika, diskusijski prispevek - option( - value=122 - selected=extraction.documentType.includes(122) - ) Intervju - option( - value=123 - selected=extraction.documentType.includes(123) - ) Umetniški sestavek - option( - value=124 - selected=extraction.documentType.includes(124) - ) Bibliografija, kazalo ipd. - option( - value=125 - selected=extraction.documentType.includes(125) - ) Drugi članki ali sestavki - option( - value=201 - selected=extraction.documentType.includes(201) - ) Znanstvena monografija - option( - value=202 - selected=extraction.documentType.includes(202) - ) Strokovna monografija - option( - value=203 - selected=extraction.documentType.includes(203) - ) Univerzitetni, visokošolski ali višješolski učbenik z recenzijo - option( - value=204 - selected=extraction.documentType.includes(204) - ) Srednješolski, osnovnošolski ali drugi učbenik z recenzijo - option( - value=205 - selected=extraction.documentType.includes(205) - ) Video in druga učna gradiva - option( - value=206 - selected=extraction.documentType.includes(206) - ) Enciklopedija, slovar, leksikon, priročnik, atlas, zemljevid - option( - value=207 - selected=extraction.documentType.includes(207) - ) Bibliografija - option( - value=208 - selected=extraction.documentType.includes(208) - ) Doktorska disertacija - option( - value=209 - selected=extraction.documentType.includes(209) - ) Magistrsko delo - option( - value=210 - selected=extraction.documentType.includes(210) - ) Specialistično delo - option( - value=211 - selected=extraction.documentType.includes(211) - ) Diplomsko delo - option( - value=212 - selected=extraction.documentType.includes(212) - ) Končno poročilo o rezultatih raziskav - option( - value=213 - selected=extraction.documentType.includes(213) - ) Elaborat, predštudija, študija - option( - value=214 - selected=extraction.documentType.includes(214) - ) Projektna dokumentacija (idejni projekt, izvedbeni projekt) - option( - value=215 - selected=extraction.documentType.includes(215) - ) Izvedensko mnenje, arbitražna odločba - option( - value=216 - selected=extraction.documentType.includes(216) - ) Umetniško delo - option( - value=217 - selected=extraction.documentType.includes(217) - ) Katalog razstave - option( - value=218 - selected=extraction.documentType.includes(218) - ) Raziskovalni ali dokumentarni film, zvočni ali video posnetek - option( - value=219 - selected=extraction.documentType.includes(219) - ) Radijska ali televizijska oddaja - option( - value=220 - selected=extraction.documentType.includes(220) - ) Raziskovalni podatki - option( - value=221 - selected=extraction.documentType.includes(221) - ) Programska oprema - option( - value=222 - selected=extraction.documentType.includes(222) - ) Nova sorta - option( - value=223 - selected=extraction.documentType.includes(223) - ) Patentna prijava - option( - value=224 - selected=extraction.documentType.includes(224) - ) Patent - option( - value=225 - selected=extraction.documentType.includes(225) - ) Druge monografije in druga zaključena dela - option( - value=227 - selected=extraction.documentType.includes(227) - ) Znanstveni terminološki slovar, enciklopedija ali tematski leksikon - option( - value=230 - selected=extraction.documentType.includes(230) - ) Zbornik strokovnih ali nerecenziranih znanstvenih prispevkov na konferenci - option( - value=231 - selected=extraction.documentType.includes(231) - ) Zbornik recenziranih znanstvenih prispevkov na mednarodni ali tuji konferenci - option( - value=232 - selected=extraction.documentType.includes(232) - ) Zbornik recenziranih znanstvenih prispevkov na domači konferenci - option( - value=310 - selected=extraction.documentType.includes(310) - ) Umetniška poustvaritev - option( - value=311 - selected=extraction.documentType.includes(311) - ) Radijski ali TV dogodek - option( - value=312 - selected=extraction.documentType.includes(312) - ) Razstava - option( - value=313 - selected=extraction.documentType.includes(313) - ) Organiziranje znanstvenih in strokovnih sestankov - option( - value=314 - selected=extraction.documentType.includes(314) - ) Predavanje na tuji univerzi - option( - value=315 - selected=extraction.documentType.includes(315) - ) Prispevek na konferenci brez natisa - option( - value=316 - selected=extraction.documentType.includes(316) - ) Vabljeno predavanje na konferenci brez natisa - option( - value=320 - selected=extraction.documentType.includes(320) - ) Druga dela - option( - value=325 - selected=extraction.documentType.includes(325) - ) Druga izvedena dela + each type in ossDocumentTypes + option( + value=type.id + selected=extraction.documentType.includes(type.id) + )= type.name .col-sm.d-flex.align-items-center span.d-md-inline.d-block.smaller-gray-info.ms-xxl-3.ms-md-3= t('Izberite vrsto dokumenta iz seznama, npr. članek, diplomsko delo.') diff --git a/express/views/pages/help-pug-demo-frame.pug b/express/views/pages/help-pug-demo-frame.pug deleted file mode 100644 index 8c638ec..0000000 --- a/express/views/pages/help-pug-demo-frame.pug +++ /dev/null @@ -1,8 +0,0 @@ -// DEMO?? -//- TODO MARK FOR DELETION - -extends /layout - -block body - .help-content-container(style="margin: 0.5rem") - include help-pug-demo diff --git a/express/views/pages/help-pug-demo.pug b/express/views/pages/help-pug-demo.pug deleted file mode 100644 index 48c4061..0000000 --- a/express/views/pages/help-pug-demo.pug +++ /dev/null @@ -1,190 +0,0 @@ -//- TODO MARK FOR DELETION - - - -h2 Iskanje - -p bold #[b adasdasda] -p bold+ #[b dasdas#[sup 1] dasd#[sub 2]] -p italic #[i sddasd] -p italic+ #[i dadsasda#[sup 1] dsada#[sub 2]] -p bold+italic #[b #[i sdasd]] -p bold+italic+ #[b #[i dsda#[sup 1] sddasd#[sub 2]]] -p Xxxxx #[a(href="https://www.amebis.si/" target="_blank") Amebis] -p Xxxxx #[a(href="https://www.amebis.si/") Amebis (v istem zavihku)] -p adasda #[b #[a(href="https://www.amebis.si/" target="_blank") Amebis]] -p as #[i #[a(href="https://www.amebis.si/" target="_blank") Amebis]] -p adada #[b #[i #[a(href="https://www.amebis.si/" target="_blank") Amebis]]] - -p Xxxxx #[a(href="https://www.amebis.si/" target="_blank") Amebis] asd as das da sd as da s d a sd as d a s d as - //- To je komentar, ki se ne interpretira. - //- Vrstica se lahko v pug sintaksi lomi na sledeč način, vendar pozor: - //- Lom v sintaksi je namenjen preglednejši sintaksi, NE prevede pa se v lom, temveč v presledek, - //- zato je lom v sintaksi smiselno narediti namesto presledka v želenem besedilu. - | #[b dasdas#[sup 1] dasd#[sub 2]] a sdasdasd asd asd as das d a sd a sd a s d a s d a s d a sd #[b adasdasda] - | d asddsda #[i sddasd] adada #[i dadsasda#[sup 1] dsada#[sub 2]] sd dsad adasda #[b #[a(href="https://www.amebis.si/" target="_blank") Amebis]] - | dsasd aas da d a dasdas d a #[b #[i sdasd]] a sda sd a s #[b #[i dsda#[sup 1] sddasd#[sub 2]]] adada dadsasda dsada. - -p Xxxxx - //- Če pretiravam v lomih za namen demonstracije učinka na rezultat (lomi --> presledki). - | asd - | as - | das - //- Konec pretiravanja. - //- Dolžina vrstice pug sintakse naj bo taka, da bo čimbolj pregledna (npr. ne daljša od 150 znakov). - //- Dolžina končne vrstice bo dinamična in se bo avtomatsko prilagajala trenutni širini brskalnika ter drugim dejavnikom. - | da sd as da s d a sd as d a s d as #[i #[a(href="https://www.amebis.si/" target="_blank") Amebis]] dasdasdasd a sdasdasd - | asd asd as das d a sd a sd a s d a s d a s d a sd adasdasda d asddsda sddasd adada #[b #[i #[a(href="https://www.amebis.si/" target="_blank") Amebis]]] - | dadsasda dsada sd dsad. - -p Naštevanje: -ul - li ena - li dva - ul - li ena2 - li dva2 - as dlaskldkalskjd lakls jldkj laksldjklaskj dlkajslkjd lakjs lkj dlkj lakjs lkj lkj lkj lkdjlakjs lkdj aklsd l - li tri2 - li tri -//- Če bi kje želeli večji razmak, kot običajno, lahko uporabimo enega ali več "br" elementov. -br -br -br -br -br -ol - li ena - asdasdnba sdasd as dlakskdlaks dlakskjdlkjalksj lkdj lka lsklk dlak slkj dlkaj lkj slkj dl alksjdl akslkj lkj dlkj alk lsk ldkj als dkj - li dva - ol - li ena - li dva - li tri - li tri - -p Tabela: -table - tr - th - th 1 - th 2 - th 3 - tr - td #[i a] - td a1 - td a2 - td a3 - tr - td #[i b] - td b1 - td - td b3 - -p Slika: - //- Za prikaz slike potrebujemo tudi izvorno sliko. - //- Tu uporabim sliko poimenovano "help-amebis-logo-pug-demo.png". - //- Vsebina polja "alt" se uporabi, ko slika ni na voljo ali za slepe uporabnike, ki slike ne vidijo. - //- Ta vsebina mora torej podati vso informacijo, tako, da bi lahko z njo v celoti nadomestili sliko, - //- saj jo slepi/slabovidni uporabniki slišijo na mestu, kjer je za ostale umeščena slika. - img(src="/images/help-amebis-logo-pug-demo.png", alt="Demo Amebisov logo") - - figure - img(src="/images/help-amebis-logo-pug-demo.png", alt="Demo Amebisov logo") - figcaption Podnapis - -p Konec. - -h3 Osnovno iskanje -p xxxxx - -h3 Napredno iskanje -p xxxxx - -h2 Luščenje -p xxxxx - -h3 Specializirani korpusi -p xxxxx - -h3 Lastni uporabniški korpus -p xxxxx - -h3 Korpus OSS -p xxxxx - -h3 Področja -p xxxxx - -h3 Terminološki kandidati -p xxxxx - -h2 Urejanje -p xxxxx - -h3 Nov slovar -p xxxxx - -h3 Urejanje lastnosti slovarja -p xxxxx - -h3 Uporabniki -p xxxxx - -h3 Struktura slovarskega sestavka -p xxxxx - -h3 Komentiranje -p xxxxx - -h3 Urejanje vsebine slovarja -p xxxxx - -h3 Uvoz podatkov -p xxxxx - -h3 Dodajanje novih slovarskih sestavkov -p xxxxx - -h4 Zaglavje -p xxxxx - -h4 Področne oznake -p xxxxx - -h4 Pojasnilo -p xxxxx - -h4 Definicija -p xxxxx - -h4 Sinonimi -p xxxxx - -h4 Povezave -p xxxxx - -h4 Tuji jeziki (elementi) -p xxxxx - -h4 Zgodovina urejanja -p xxxxx - -h4 Komentarji -p xxxxx - -h2 Svetovanje -p xxxxx - -h3 Pošiljanje vprašanj -p xxxxx - -h3 Terminološka načela -p xxxxx - -h3 Povezava -p xxxxx - -h3 Objava odgovorov -p xxxxx - -h2 Administracija -p xxxxx diff --git a/express/views/pages/profile/change-password.pug b/express/views/pages/profile/change-password.pug index 53ba04d..2b32dd8 100644 --- a/express/views/pages/profile/change-password.pug +++ b/express/views/pages/profile/change-password.pug @@ -16,7 +16,7 @@ block body .content-hold-prerequisites #offset-main.main-container.mt-2 - form.needs-validation(method="post" novalidate) + form#profileForm.needs-validation(method="post" novalidate) div .subject-name label.input-name-txt(for="password-old") #{ t('STARO GESLO') } @@ -24,7 +24,7 @@ block body .col-sm-6.align-items-center input#password-old.name-input.form-control.d-inline( type="password" - name="password-old" + name="passwordOld" value="" ) .invalid-feedback #{ t('Napačno geslo.') } @@ -39,7 +39,7 @@ block body .col-sm-6 input#password-new.name-input.form-control.d-inline( type="password" - name="password-new" + name="passwordNew" minLength="1" value="" ) @@ -55,7 +55,7 @@ block body .col-sm-6 input#password-repeat.name-input.form-control.d-inline( type="password" - name="password-repeat" + name="passwordNewRepeat" minLength="1" value="" ) diff --git a/express/views/pages/profile/delete-profile.pug b/express/views/pages/profile/delete-profile.pug index 13603d9..d7d6c8b 100644 --- a/express/views/pages/profile/delete-profile.pug +++ b/express/views/pages/profile/delete-profile.pug @@ -92,3 +92,5 @@ block body span.ps-2.text-white #{ t('IZBRIŠI') } include /common/footer include /utilities/modal-alert + include /utilities/modal-info + +modalInfo diff --git a/express/views/pages/profile/my-profile-side-menu-mixin.pug b/express/views/pages/profile/my-profile-side-menu-mixin.pug index 372bf5b..ee8f9a3 100644 --- a/express/views/pages/profile/my-profile-side-menu-mixin.pug +++ b/express/views/pages/profile/my-profile-side-menu-mixin.pug @@ -7,31 +7,31 @@ mixin sideNavigation(sideNavigationData) src="/images/burger-menu-button-icon.svg" alt=t('Meni') ) - span.nav-title #{ t('Moj Profil') } + span.nav-title #{ t('Moj Račun') } #mobile-right-holder nav ul.admin-nav-content.slidable.scroller-style - li( + li.p-0( class=sideNavigationData.selection === 'change-profile-data' ? 'focused-menu' : 'admin-nav-item' ) - a.active(href="/moj-racun") + a.p-2.active(href="/moj-racun") img(src="/images/user-gray.svg" alt=t('Basic')) p #{ t('Osnovni podatki') } - li( + li.p-0( class=sideNavigationData.selection === 'change-profile-password' ? 'focused-menu' : 'admin-nav-item' ) - a.active(href="/spremeni-geslo") + a.p-2.active(href="/spremeni-geslo") img(src="/images/lock-gray.svg" alt=t('Settings')) p #{ t('Spremeni geslo') } - li( + li.p-0( class=sideNavigationData.selection === 'change-profile-settings' ? 'focused-menu' : 'admin-nav-item' ) - a.active(href="/nastavitve-racuna") + a.p-2.active(href="/nastavitve-racuna") img(src="/images/cog.svg" alt=t('Settings')) p #{ t('Nastavitve') } - li( + li.p-0( class=sideNavigationData.selection === 'delete-profile' ? 'focused-menu' : 'admin-nav-item' ) - a.active(href="/izbrisi-racun") + a.p-2.active(href="/izbrisi-racun") img(src="/images/user-x.svg" alt=t('Delete profile')) p #{ t('Izbriši račun') } diff --git a/express/views/pages/profile/my-profile.pug b/express/views/pages/profile/my-profile.pug index 93c3944..01c7a4e 100644 --- a/express/views/pages/profile/my-profile.pug +++ b/express/views/pages/profile/my-profile.pug @@ -35,12 +35,12 @@ block body .mt-4 .subject-name - label.input-name-txt(for="profile-name") #{ t('IME') } + label.input-name-txt(for="profile-first-name") #{ t('IME') } .row .col-sm-6 - input#profile-name.name-input.form-control.d-inline( + input#profile-first-name.name-input.form-control.d-inline( type="text" - name="name" + name="firstName" minLength="1" value=user.firstName ) @@ -51,12 +51,12 @@ block body .mt-4 .subject-name - label.input-name-txt(for="profile-surname") #{ t('PRIIMEK') } + label.input-name-txt(for="profile-last-name") #{ t('PRIIMEK') } .row .col-sm-6 - input#profile-surname.name-input.form-control.d-inline( + input#profile-last-name.name-input.form-control.d-inline( type="text" - name="surname" + name="lastName" minLength="1" value=user.lastName ) @@ -67,10 +67,10 @@ block body .mt-4 .subject-name - label.input-name-txt(for="profile-surname") #{ t('ELEKTRONSKI NASLOV') } + label.input-name-txt(for="profile-email") #{ t('ELEKTRONSKI NASLOV') } .row .col-sm-6 - input#profile-surname.name-input.form-control.d-inline( + input#profile-email.name-input.form-control.d-inline( type="text" name="email" minLength="1" diff --git a/express/views/pages/reset-password/reset-password.pug b/express/views/pages/reset-password/reset-password.pug index d3d7a80..87267e4 100644 --- a/express/views/pages/reset-password/reset-password.pug +++ b/express/views/pages/reset-password/reset-password.pug @@ -9,7 +9,7 @@ block body +main-navigation(false) include /utilities/generic-main-panel-header - let c - if (isValidToken) + if (isTokenValid) - c = { h1: t('Ponastavi geslo'), description: t('Vnesite novo geslo. Ko ga potrdite, se boste v vaš uporabniški račun lahko spet prijavili z novim geslom.') } else - c = { h1: t('Obvestilo'), description: '' } @@ -19,12 +19,13 @@ block body #offset-main.container-fluid.mt-4.errc.px-0 .p-squeeze-lg .rpm-corr - if (isValidToken) + if (isTokenValid) // h1.navigation-text-color.mt-3 Ponastavi Geslo form#reset-and-redirect.max-512px( action="/reset-password" method="POST" ) + input#token(type="hidden" value=token) input#reset-password.mb-3.form-control( type="password" placeholder=t('Geslo') @@ -33,10 +34,10 @@ block body type="password" placeholder=t('Ponovi Geslo') ) + p#reset-password-error.error-text.mb-1 .d-flex.justify-content-between - button#cancel-btn.btn.btn-secondary.h-40px(type="button") #{ t('Prekliči') } + a.btn.btn-secondary.h-40px(href="/") #{ t('Prekliči') } button#reset-password-btn.btn.btn-primary.h-40px(type="submit") #{ t('Potrdi') } - input#token.d-none(value=token) else //h1.navigation-text-color.mt-3 Obvestilo p #{ t('Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.') } diff --git a/express/views/utilities/consultancy-admin-in-progress-panel-header.pug b/express/views/utilities/consultancy-admin-in-progress-panel-header.pug index c092c7b..c15d461 100644 --- a/express/views/utilities/consultancy-admin-in-progress-panel-header.pug +++ b/express/views/utilities/consultancy-admin-in-progress-panel-header.pug @@ -7,7 +7,7 @@ mixin topHeaderContent(headerContent) href=headerContent.hrefurl ) img.align-middle(src="/images/chevrons-left.svg") - span.ms-2.text-header-description-gray.fw-700 Nazaj + span.ms-2.text-header-description-gray.fw-700= t('Nazaj') .row.w-100.mx-0 .col-sm-8.g-0 .header-container-divider-left.pt-1 diff --git a/express/views/utilities/content-edit.pug b/express/views/utilities/content-edit.pug index 4b1c67e..3a2c782 100644 --- a/express/views/utilities/content-edit.pug +++ b/express/views/utilities/content-edit.pug @@ -254,7 +254,7 @@ form#form-edit-content.needs-validatio.mt-2( each language, index in languages hr(class=index ? 'hr-languages' : 'd-none') .language.px-3(data-langauge-id=language.id) - span.bold-blue-text= language.nameSl + span.bold-blue-text= language.name input( type="hidden" name=`foreign[${language.id}][code]` @@ -300,7 +300,7 @@ form#form-edit-content.needs-validatio.mt-2( span.d-md-inline.d-block.name-info-txt.mt-3.mt-sm-0= t('Dodajte tujejezične ustreznike, ki se za opisani pojem tudi uporabljajo v tujem jeziku.') else .language.px-3 - span.bold-blue-text= languages.nameSl + span.bold-blue-text= languages.name .subject-name.d-flex.flex-column.mt-2 label.input-name-txt.mt-2(for="foreign-term")= t('TERMINI') .row diff --git a/express/views/utilities/dictionary-description-mixin.pug b/express/views/utilities/dictionary-description-mixin.pug index 6061204..3181e00 100644 --- a/express/views/utilities/dictionary-description-mixin.pug +++ b/express/views/utilities/dictionary-description-mixin.pug @@ -168,7 +168,7 @@ mixin dictionary-description(dictionaryId) option( value=domain.id selected=domain.id === dictionary.domainPrimary - )= domain.nameSl + )= domain.name #invalid-section.invalid-feedback-selection.hidden-section.mt-3= t('Nimate izbranega področja CERIF.') .col-sm.d-flex.align-items-center span.d-md-inline.d-block.smaller-gray-info.ms-xxl-3.ms-md-3= t('Izberite področje svojega terminološkega slovarja na seznamu področij.') @@ -184,9 +184,9 @@ mixin dictionary-description(dictionaryId) ) each domain in allSecondaryDomains if associatedSecondaryDomains.some(associatedDomain => associatedDomain.id === domain.id) - option(value=domain.id selected)= domain.nameSl + option(value=domain.id selected)= domain[`name${capitalize(determinedLanguage)}`] else - option(value=domain.id)= domain.nameSl + option(value=domain.id)= domain[`name${capitalize(determinedLanguage)}`] .col-sm.d-flex.align-items-center span.d-md-inline.d-block.smaller-gray-info.ms-xxl-3.ms-md-3= t('Podpodročje.') diff --git a/express/views/utilities/dictionary-extraction-import-mixin.pug b/express/views/utilities/dictionary-extraction-import-mixin.pug index 3055508..0ef781f 100644 --- a/express/views/utilities/dictionary-extraction-import-mixin.pug +++ b/express/views/utilities/dictionary-extraction-import-mixin.pug @@ -12,7 +12,7 @@ mixin dictionary-extraction-import option(value=el.id)= el.name .col-sm.d-flex.align-items-center span.name-info-txt.ms-lg-3.mt-2.mt-lg-0= t('Izberite enega od rezultatov luščenja s seznama.') - .list-terminology-candidates.mt-4.me-2 + .list-terminology-candidates.mt-4.me-2.d-none .d-flex.w-100.justify-content-between .d-flex.align-items-center span.info-text-for-button= t('Seznam terminoloških kandidatov') diff --git a/express/views/utilities/dictionary-main-input.pug b/express/views/utilities/dictionary-main-input.pug index d107ca2..92e96dd 100644 --- a/express/views/utilities/dictionary-main-input.pug +++ b/express/views/utilities/dictionary-main-input.pug @@ -82,7 +82,7 @@ mixin dictionaryMainInput(data) //- TODO I18n option(value="" hidden disabled selected)= t('Izberite področje') each domain in allPrimaryDomains - option(value=domain.id)= domain.nameSl + option(value=domain.id)= domain.name #invalid-section.invalid-feedback-selection.hidden-section.mt-3= t('Niste izbrali glavnega področja.') .col-sm.d-flex.align-items-center span.d-md-inline.d-block.name-info-txt.ms-xxl-3.ms-md-3.mt-3.mt-sm-0= t('Področje.') @@ -98,7 +98,7 @@ mixin dictionaryMainInput(data) ) //- TODO I18n each domain in allSecondaryDomains - option(value=domain.id)= domain.nameSl + option(value=domain.id)= domain[`name${capitalize(determinedLanguage)}`] .col-sm.d-flex.align-items-center span.d-md-inline.d-block.name-info-txt.ms-xxl-3.ms-md-3.mt-3.mt-sm-0= t('Vpišite podpodročje glavnega področja, ki ste ga izbrali. Če podpodročja ni med naborom, izberite polje Novo podpodročje.') diff --git a/express/views/utilities/dictionary-structure-input.pug b/express/views/utilities/dictionary-structure-input.pug index b67e2ee..76a3552 100644 --- a/express/views/utilities/dictionary-structure-input.pug +++ b/express/views/utilities/dictionary-structure-input.pug @@ -336,17 +336,16 @@ block input-structure if dictionary if dictionary.hasForeignLanguages .col-sm-6.select-languages - //- TODO I18n select#languages-input.without-addition( name="language" multiple required ) each language in associatedLanguages - option(value=language.id selected)= language.nameSl + option(value=language.id selected)= language.name each language in allLanguages if !associatedLanguages.some(associated => associated.id === language.id) - option(value=language.id)= language.nameSl + option(value=language.id)= language.name else .col-sm-6.select-languages select#languages-input.without-addition( @@ -356,7 +355,7 @@ block input-structure required ) each language in allLanguages - option(value=language.id)= language.nameSl + option(value=language.id)= language.name else .col-sm-6.select-languages select#languages-input.form-control.without-addition( @@ -365,7 +364,7 @@ block input-structure required ) each language in allLanguages - option(value=language.id)= language.nameSl + option(value=language.id)= language.name .col-sm.d-flex.align-items-center span.d-md-inline.d-block.name-info-txt.ms-xxl-3.ms-md-3.mt-3.mt-sm-0= t('Izberite vse tuje jezike, ki jih bo terminološki slovar vseboval.') #invalid-language-section.invalid-feedback-selection.hidden-section.mt-3 diff --git a/express/views/utilities/entry-preview-mixin.pug b/express/views/utilities/entry-preview-mixin.pug index cf0e095..8a609f0 100644 --- a/express/views/utilities/entry-preview-mixin.pug +++ b/express/views/utilities/entry-preview-mixin.pug @@ -66,7 +66,7 @@ mixin entryPreview each entry, index in entryData.foreign_entries hr(class=index ? 'd-none' : '') .preview-foreign-language - span.smaller-black-info.text-break!= entry.name_sl + span.smaller-black-info.text-break!= entry.name if (structure.hasForeignLanguages && entry.term && entry.term.join(", ").length) .preview-foreign-terms.mt-1.d-flex.align-items-start.row .d-sm-block.col-sm-4.preview-column-width diff --git a/express/views/utilities/entry-preview.pug b/express/views/utilities/entry-preview.pug index bc6f368..25e4a0a 100644 --- a/express/views/utilities/entry-preview.pug +++ b/express/views/utilities/entry-preview.pug @@ -66,11 +66,11 @@ if (structure.hasForeignLanguages) .preview-languages-container.mt-2 if (languages.length) - hr.language-line + hr.start-line each language, index in languages .preview-one-language.d-none .preview-foreign-language - span.smaller-black-info= language.nameSl + span.smaller-black-info= language.name .preview-foreign-terms.mt-1.d-flex.align-items-baseline.row .d-sm-block.col-sm-4.preview-column-width span.smaller-gray-info= t('TERMIN:') diff --git a/express/views/utilities/modal-info.pug b/express/views/utilities/modal-info.pug new file mode 100644 index 0000000..e7ca1ad --- /dev/null +++ b/express/views/utilities/modal-info.pug @@ -0,0 +1,27 @@ +mixin modalInfo(id="info-modal", text=t(''), title=t('Obvestilo'), btnText=t('Zapri')) + .modal.fade( + id=id + tabindex="-1" + aria-labelledby="modal-alert-label" + aria-hidden="true" + ) + .modal-dialog.modal-lg + .modal-content + .modal-header + h5#modal-info-label.modal-title.modal-title-blue #{ title } + button.btn-close( + type="button" + data-bs-dismiss="modal" + aria-label="Close" + ) + .modal-body.d-flex.align-items-center + .d-flex.modal-alert.me-3 + img(src="/images/alert-circle-big.svg") + .d-flex.modal-alert-content + p#info-text.normal-gray #{ text } + + .modal-footer.d-flex.justify-content-center.align-items-center + button#modal-fp-info-close.btn.btn-primary( + type="button" + data-bs-dismiss="modal" + ) #{ btnText } diff --git a/express/views/utilities/modal-reset-password-info.pug b/express/views/utilities/modal-reset-password-info.pug index 239deed..32e0c7c 100644 --- a/express/views/utilities/modal-reset-password-info.pug +++ b/express/views/utilities/modal-reset-password-info.pug @@ -1,22 +1,22 @@ #reset-pass-info.modal.fade( tabindex="-1" - aria-labelledby="modal-alert-label" + aria-labelledby="modal-fpi-label" aria-hidden="true" ) .modal-dialog.modal-lg .modal-content .modal-header - h5#modal-alert-label.modal-title.modal-title-blue #{ t('Obvestilo') } + h5#modal-fpi-label.modal-title.modal-title-blue #{ t('Obvestilo') } button.btn-close( type="button" data-bs-dismiss="modal" aria-label="Close" ) .modal-body.d-flex.align-items-center - .d-flex.modal-alert.me-3 + .d-flex.modal-fpi.me-3 img(src="/images/alert-circle-modal.svg") - .d-flex.modal-alert-content - p#alert-text.normal-gray #{ t('Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.') } + .d-flex.modal-fpi-content + p#fpi-text.normal-gray #{ t('Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.') } .modal-footer.d-flex.justify-content-center.align-items-center button#modal-fp-info-close.btn.btn-primary( diff --git a/express/views/utilities/modal-reset-password-success.pug b/express/views/utilities/modal-reset-password-success.pug index 9c03a44..66027bc 100644 --- a/express/views/utilities/modal-reset-password-success.pug +++ b/express/views/utilities/modal-reset-password-success.pug @@ -16,7 +16,7 @@ .d-flex.modal-alert.me-3 img(src="/images/alert-circle-modal.svg") .d-flex.modal-alert-content - p#alert-text.normal-gray #{ t('Uspešno ste ponastavili svoje geslo.') } + p#fpi-text.normal-gray #{ t('Uspešno ste ponastavili svoje geslo.') } .modal-footer.d-flex.justify-content-center.align-items-center button#modal-fp-info-close.btn.btn-primary( diff --git a/express/views/utilities/modal-reset-password.pug b/express/views/utilities/modal-reset-password.pug index 1e126be..55eee15 100644 --- a/express/views/utilities/modal-reset-password.pug +++ b/express/views/utilities/modal-reset-password.pug @@ -6,7 +6,7 @@ .modal-dialog.modal-lg .modal-content .modal-header - h5#modal-alert-label.modal-title.modal-title-blue #{ t('Pozabljeno geslo') } + h5#modal-fp-label.modal-title.modal-title-blue #{ t('Pozabljeno geslo') } button.btn-close( type="button" data-bs-dismiss="modal" diff --git a/express/views/utilities/result-detail-side-menu.pug b/express/views/utilities/result-detail-side-menu.pug index 55542c6..59946a8 100644 --- a/express/views/utilities/result-detail-side-menu.pug +++ b/express/views/utilities/result-detail-side-menu.pug @@ -43,10 +43,10 @@ mixin result_detail(data) li.text-gray-1.fw-500 #{ t('Jeziki') } .text-header-description-gray.fw-400= data.languages if data.time_modified - li.text-gray-1.fw-500 Spremenjen + li.text-gray-1.fw-500 #{ t('Spremenjen') } .text-header-description-gray.fw-400= data.time_modified if data.count_entries - li.text-gray-1.fw-500 Število slovarskih sestavkov + li.text-gray-1.fw-500 #{ t('Število slovarskih sestavkov') } .text-header-description-gray.fw-400= data.count_entries if data.issn li.text-gray-1.fw-500 #{ t('ISSN') } diff --git a/postgres/init/006_auth_and_aut.sql b/postgres/init/006_auth_and_aut.sql index 83867c7..536cda4 100644 --- a/postgres/init/006_auth_and_aut.sql +++ b/postgres/init/006_auth_and_aut.sql @@ -6,7 +6,7 @@ DROP TYPE IF EXISTS user_role_name; DROP TYPE IF EXISTS user_hits_per_page; DROP TYPE IF EXISTS user_status; -CREATE TYPE user_status AS ENUM ('registered', 'active', 'inactive'); +CREATE TYPE user_status AS ENUM ('registered', 'active', 'inactive', 'closed'); CREATE TYPE user_hits_per_page AS ENUM ('10', '20', '50', '100'); @@ -24,6 +24,7 @@ CREATE TABLE "user" ( bcrypt_hash VARCHAR NOT NULL, time_registered TIMESTAMPTZ NOT NULL DEFAULT NOW(), time_activated TIMESTAMPTZ, + time_closed TIMESTAMPTZ, hits_per_page user_hits_per_page NOT NULL DEFAULT '10', language user_language NOT NULL DEFAULT 'sl' ); @@ -40,6 +41,19 @@ CREATE TABLE user_token_remember_me ( time_created TIMESTAMPTZ NOT NULL DEFAULT NOW() ); +CREATE TABLE user_token_reset_password ( + token VARCHAR PRIMARY KEY, + user_id INT NOT NULL REFERENCES "user", + time_created TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE user_token_change_email ( + token VARCHAR PRIMARY KEY, + user_id INT NOT NULL REFERENCES "user", + new_email VARCHAR NOT NULL, + time_created TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + CREATE TABLE user_role ( user_id INT NOT NULL REFERENCES "user", role_name user_role_name NOT NULL, diff --git a/postgres/init/018_extraction.sql b/postgres/init/018_extraction.sql index 9e7e66e..a19d420 100644 --- a/postgres/init/018_extraction.sql +++ b/postgres/init/018_extraction.sql @@ -16,7 +16,7 @@ CREATE TABLE extraction ( time_finished TIMESTAMPTZ, CHECK ( CASE - WHEN oss_params IS NULL AND status = 'finished' THEN corpus_id IS NOT NULL + WHEN oss_params IS NULL THEN TRUE ELSE corpus_id IS NULL END ) diff --git a/postgres/init/019_extraction_oss_document_types.sql b/postgres/init/019_extraction_oss_document_types.sql new file mode 100644 index 0000000..cd74866 --- /dev/null +++ b/postgres/init/019_extraction_oss_document_types.sql @@ -0,0 +1,68 @@ +CREATE TABLE extraction_oss_document_types ( + id SMALLINT PRIMARY KEY, + name_sl VARCHAR NOT NULL UNIQUE, + name_en VARCHAR NOT NULL UNIQUE +); + +INSERT INTO extraction_oss_document_types (id, name_sl, name_en) +VALUES + (101, 'Izvirni znanstveni članek', 'Original Scientific Article'), + (102, 'Pregledni znanstveni članek', 'Review Article'), + (103, 'Kratki znanstveni prispevek', 'Short Scientific Article'), + (104, 'Strokovni članek', 'Professional Article'), + (105, 'Poljudni članek', 'Popular Article'), + (106, 'Objavljeni znanstveni prispevek na konferenci (vabljeno predavanje)', 'Published Scientific Conference Contribution (invited lecture)'), + (107, 'Objavljeni strokovni prispevek na konferenci (vabljeno predavanje)', 'Published Professional Conference Contribution (invited lecture)'), + (108, 'Objavljeni znanstveni prispevek na konferenci', 'Published Scientific Conference Contribution'), + (109, 'Objavljeni strokovni prispevek na konferenci', 'Published Professional Conference Contribution'), + (110, 'Objavljeni povzetek znanstvenega prispevka na konferenci (vabljeno predavanje)', 'Published Scientific Conference Contribution Abstract (invited lecture)'), + (112, 'Objavljeni povzetek znanstvenega prispevka na konferenci', 'Published Scientific Conference Contribution Abstract'), + (113, 'Objavljeni povzetek strokovnega prispevka na konferenci', 'Published Professional Conference Contribution Abstract'), + (116, 'Samostojni znanstveni sestavek ali poglavje v monografski publikaciji', 'Independent Scientific Component Part or a Chapter in a Monograph'), + (117, 'Samostojni strokovni sestavek ali poglavje v monografski publikaciji', 'Independent Professional Component Part or a Chapter in a Monograph'), + (118, 'Geslo – sestavek v enciklopediji, leksikonu, slovarju...', 'Encyclopaedia, Lexicon, Dictionary (and the like) Entry'), + (119, 'Recenzija, prikaz knjige, kritika', 'Review, Book Review, Critique'), + (120, 'Predgovor, spremna beseda', 'Preface, Afterword'), + (121, 'Polemika, diskusijski prispevek', 'Polemic, Discussion'), + (122, 'Intervju', 'Interview'), + (123, 'Umetniški sestavek', 'Artistic Component Part'), + (124, 'Bibliografija, kazalo ipd.', 'Bibliography, Index, etc.'), + (125, 'Drugi članki ali sestavki', 'Other Component Parts'), + (201, 'Znanstvena monografija', 'Scientific Monograph'), + (202, 'Strokovna monografija', 'Professional Monograph'), + (203, 'Univerzitetni, visokošolski ali višješolski učbenik z recenzijo', 'Reviewed University, Higher Education or Higher Vocational Education Textbook'), + (204, 'Srednješolski, osnovnošolski ali drugi učbenik z recenzijo', 'Reviewed Secondary and Primary School Textbook or Other Textbook'), + (205, 'Video in druga učna gradiva', 'Videolectures and teaching material'), + (206, 'Enciklopedija, slovar, leksikon, priročnik, atlas, zemljevid', 'Encyclopaedia, Dictionary, Lexicon, Manual, Atlas, Map'), + (207, 'Bibliografija', 'Bibliography'), + (208, 'Doktorska disertacija', 'Doctoral Dissertation'), + (209, 'Magistrsko delo', 'Master''s Thesis'), + (210, 'Specialistično delo', 'Specialist Thesis'), + (211, 'Diplomsko delo', 'Undergraduate Thesis'), + (212, 'Končno poročilo o rezultatih raziskav', 'Final Research Report'), + (213, 'Elaborat, predštudija, študija', 'Treatise, Preliminary Study, Study'), + (214, 'Projektna dokumentacija (idejni projekt, izvedbeni projekt)', 'Project Documentation (preliminary design, working design)'), + (215, 'Izvedensko mnenje, arbitražna odločba', 'Expertise, Arbitration Decision'), + (216, 'Umetniško delo', 'Artistic Work'), + (217, 'Katalog razstave', 'Exhibition Catalogue'), + (218, 'Raziskovalni ali dokumentarni film, zvočni ali video posnetek', 'Scientific or Documentary Film, Sound or Video Recording'), + (219, 'Radijska ali televizijska oddaja', 'Radio or Television Broadcast'), + (220, 'Raziskovalni podatki', 'Research Data'), + (221, 'Programska oprema', 'Software'), + (222, 'Nova sorta', 'New variety'), + (223, 'Patentna prijava', 'Patent Application'), + (224, 'Patent', 'Patent'), + (225, 'Druge monografije in druga zaključena dela', 'Other Monographs and Other Completed Works'), + (227, 'Znanstveni terminološki slovar, enciklopedija ali tematski leksikon', 'Znanstveni terminološki slovar, enciklopedija ali tematski leksikon'), + (230, 'Zbornik strokovnih ali nerecenziranih znanstvenih prispevkov na konferenci', 'Proceedings of Professional or Unreviewed Scientific Conference Contributions'), + (231, 'Zbornik recenziranih znanstvenih prispevkov na mednarodni ali tuji konferenci', 'Proceedings of Peer-Reviewed Scientific Conference Contributions (international and foreign conferences)'), + (232, 'Zbornik recenziranih znanstvenih prispevkov na domači konferenci', 'Proceedings of Peer-Reviewed Scientific Conference Contributions (domestic conferences)'), + (310, 'Umetniška poustvaritev', 'Artistic Performance'), + (311, 'Radijski ali TV dogodek', 'Radio or Television Event'), + (312, 'Razstava', 'Exhibition'), + (313, 'Organiziranje znanstvenih in strokovnih sestankov', 'Organizing scientific and professional meetings'), + (314, 'Predavanje na tuji univerzi', 'Invited Lecture at Foreign University'), + (315, 'Prispevek na konferenci brez natisa', 'Unpublished Conference Contribution'), + (316, 'Vabljeno predavanje na konferenci brez natisa', 'Unpublished Invited Conference Lecture'), + (320, 'Druga dela', 'Other Works'), + (325, 'Druga izvedena dela', 'Other Performed Works'); diff --git a/postgres/init/019_dictionary_export.sql b/postgres/init/020_dictionary_export.sql similarity index 100% rename from postgres/init/019_dictionary_export.sql rename to postgres/init/020_dictionary_export.sql