Fix most glaring bugs and vulnerabilities
This commit is contained in:
+20
-240
@@ -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 <commentCount> 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
|
||||
|
||||
@@ -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;`
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+47
-93
@@ -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)})`
|
||||
|
||||
|
||||
+124
-29
@@ -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
|
||||
|
||||
@@ -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="?(?<url>https?:\/\/.*?)"?[\s>]/)
|
||||
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
|
||||
|
||||
return `<a href="${url || ''}" target="_blank">`
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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="?(?<url>https?:\/\/.*?)"?[\s>]/)
|
||||
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
|
||||
|
||||
return `<a href="${url || ''}" target="_blank">`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
+314
-38
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user