diff --git a/ansible/roles/schulcloud-client-core/templates/client-configmap-files.yml.j2 b/ansible/roles/schulcloud-client-core/templates/client-configmap-files.yml.j2 index e3743bfbbe..14ced4a1e3 100644 --- a/ansible/roles/schulcloud-client-core/templates/client-configmap-files.yml.j2 +++ b/ansible/roles/schulcloud-client-core/templates/client-configmap-files.yml.j2 @@ -97,7 +97,7 @@ data: }, '^/impressum': { }, - '^/datenschutz': { + '^/privacypolicy': { }, */ '^/about': { defaultSrc: 'https://www10-fms.hpi.uni-potsdam.de https://cloud-instances.s3.hidrive.strato.com https://s3.hidrive.strato.com', diff --git a/config/http-headers.js b/config/http-headers.js index 0961d28c82..95d2e360a6 100644 --- a/config/http-headers.js +++ b/config/http-headers.js @@ -80,7 +80,7 @@ const config = { }, '^/impressum': { }, - '^/datenschutz': { + '^/privacypolicy': { }, */ '^/about': { defaultSrc: 'https://www10-fms.hpi.uni-potsdam.de https://cloud-instances.s3.hidrive.strato.com', diff --git a/controllers/dataprivacy.js b/controllers/dataprivacy.js index d3d926d8fe..562e5813c0 100644 --- a/controllers/dataprivacy.js +++ b/controllers/dataprivacy.js @@ -1,60 +1,22 @@ const express = require('express'); const { URL } = require('url'); -const api = require('../api'); -const authHelper = require('../helpers/authentication'); const { DOCUMENT_BASE_DIR, SC_THEME } = require('../config/global'); const { specificFiles } = require('../config/documents'); +const { getBase64File } = require('../helpers/fileHelper'); +const { getConsentVersion } = require('../helpers/consentVersionHelper'); const router = express.Router(); -const privacyUrl = () => { - return new URL(`${SC_THEME}/${specificFiles.privacyExemplary}`, DOCUMENT_BASE_DIR); -}; - -const downloadPolicyPdf = (res, fileData, fileTitle) => { - // ERR_INVALID_CHAR will get thrown on ukrainian translation without encoding - const encodedFileTitle = encodeURI(fileTitle); - const download = Buffer.from(fileData, 'base64'); - res.writeHead(200, { - 'Content-Type': 'application/pdf', - 'Content-Disposition': `attachment; filename="${encodedFileTitle}.pdf"`, - }).end(download); -}; - -const getBase64File = async (req, res, fileId, fileTitle) => { - if (fileId) { - const base64File = await api(req).get(`/base64Files/${fileId}`); - if (base64File.data) { - const fileData = base64File.data.replace( - 'data:application/pdf;base64,', - '', - ); - downloadPolicyPdf(res, fileData, fileTitle); - } - } -}; +const privacyUrl = () => new URL(`${SC_THEME}/${specificFiles.privacyExemplary}`, DOCUMENT_BASE_DIR); router.get('/', async (req, res, next) => { try { - const isAuthenticated = await authHelper.isAuthenticated(req); - const qs = { - $limit: 1, - consentTypes: 'privacy', - $sort: { - publishedAt: -1, - }, - }; - - if (isAuthenticated && res.locals.currentSchool) { - qs.schoolId = res.locals.currentSchool; - } - - const consentVersions = await api(req).get('/consentVersions', { qs }); + const consentVersions = await getConsentVersion(req, res, 'privacy'); if (consentVersions.data.length) { const fileId = consentVersions.data[0].consentDataId; if (!fileId) { - res.redirect(privacyUrl()); + res.redirect(privacyUrl().toString()); } const fileTitle = res.locals.theme.name === 'thr' @@ -63,7 +25,7 @@ router.get('/', async (req, res, next) => { await getBase64File(req, res, fileId, fileTitle); } else { - res.redirect(privacyUrl()); + res.redirect(privacyUrl().toString()); } } catch (err) { next(err); diff --git a/controllers/firstLogin.js b/controllers/firstLogin.js index 9140f9b838..89e79bb008 100644 --- a/controllers/firstLogin.js +++ b/controllers/firstLogin.js @@ -29,10 +29,14 @@ const hasAccount = (req, res) => api(req).get('/consents', { }, }); -const getSchoolPrivacy = async (req, res) => { +const getSchoolConsentVersionByType = async (req, res, consentType) => { + if (consentType !== 'privacy' && consentType !== 'termsOfUse') { + return undefined; + } + const qs = { schoolId: res.locals.currentUser.schoolId, - consentTypes: ['privacy'], + consentTypes: [consentType], consentDataId: { $exists: true }, $limit: 1, $sort: { @@ -249,7 +253,8 @@ router.get('/', async (req, res, next) => { sso: !!(res.locals.currentPayload || {}).systemId, now: Date.now(), sections: sections.map((name) => `firstLogin/sections/${name}`), - schoolPrivacyLink: await getSchoolPrivacy(req, res), + schoolPrivacyLink: await getSchoolConsentVersionByType(req, res, 'privacy'), + schoolTermsLink: await getSchoolConsentVersionByType(req, res, 'termsOfUse'), schoolPrivacyName: res.$t('global.text.dataProtection'), submitPageIndex, userConsent, diff --git a/controllers/index.js b/controllers/index.js index b608184b85..c885f5e596 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -33,7 +33,8 @@ router.use('/partner/', require('./partner')); router.use('/community/', require('./community')); router.use('/about/', require('./about')); router.use('/help/', require('./help')); -router.use('/datenschutz/', require('./dataprivacy')); +router.use('/privacypolicy/', require('./dataprivacy')); +router.use('/termsofuse/', require('./termsofuse')); router.use('/my-material', require('./my-material')); router.use('/base64Files', require('./base64Files')); router.use('/logs', require('./logs')); diff --git a/controllers/registration.js b/controllers/registration.js index 722a58cb1a..2fa0dfdbed 100644 --- a/controllers/registration.js +++ b/controllers/registration.js @@ -49,11 +49,11 @@ const checkValidRegistration = async (req) => { */ router.get(['/register', '/register/*'], (req, res, next) => res.render('registration/deprecated_warning')); -const getSchoolPrivacy = async (req, res) => { +const getSchoolConsentVersionByType = async (req, res, consentType) => { const importHash = getImportHash(req); try { - const consentVersion = await api(req).get(`/registration/consent/${importHash}`); + const consentVersion = await api(req).get(`/registration/consent/${importHash}?consentType=${consentType}`); if (consentVersion) { const { consentDataId } = consentVersion; return consentDataId ? `/base64Files/${consentDataId}` : undefined; @@ -251,8 +251,10 @@ router.get(['/registration/:classOrSchoolId/byparent', '/registration/:classOrSc hideMenu: true, user, needConsent, - schoolPrivacyLink: await getSchoolPrivacy(req, res), - schoolPrivacyName: res.$t('global.text.dataProtection'), + schoolPrivacyLink: await getSchoolConsentVersionByType(req, res, 'privacy'), + schoolTermsLink: await getSchoolConsentVersionByType(req, res, 'termsOfUse'), + schoolPrivacyName: res.$t('global.text.dataProtectionFile'), + schoolTermsName: res.$t('global.text.termsOfUseFile'), sectionNumber, CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS, invalid, @@ -317,8 +319,10 @@ router.get(['/registration/:classOrSchoolId/bystudent', '/registration/:classOrS user, needConsent, sectionNumber, - schoolPrivacyLink: await getSchoolPrivacy(req, res), - schoolPrivacyName: res.$t('global.text.dataProtection'), + schoolPrivacyLink: await getSchoolConsentVersionByType(req, res, 'privacy'), + schoolTermsLink: await getSchoolConsentVersionByType(req, res, 'termsOfUse'), + schoolPrivacyName: res.$t('global.text.dataProtectionFile'), + schoolTermsName: res.$t('global.text.termsOfUseFile'), CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS, invalid, secure, @@ -393,8 +397,10 @@ router.get(['/registration/:classOrSchoolId/:byRole'], async (req, res) => { user, needConsent, sectionNumber, - schoolPrivacyLink: await getSchoolPrivacy(req, res), - schoolPrivacyName: res.$t('global.text.dataProtection'), + schoolPrivacyLink: await getSchoolConsentVersionByType(req, res, 'privacy'), + schoolTermsLink: await getSchoolConsentVersionByType(req, res, 'termsOfUse'), + schoolPrivacyName: res.$t('global.text.dataProtectionFile'), + schoolTermsName: res.$t('global.text.termsOfUseFile'), invalid, secure, correctID, @@ -432,8 +438,10 @@ router.get(['/registration/:classOrSchoolId', '/registration/:classOrSchoolId/:s sso: req.params.sso === 'sso', account: req.params.accountId || '', CONSENT_WITHOUT_PARENTS_MIN_AGE_YEARS, - schoolPrivacyLink: await getSchoolPrivacy(req, res), - schoolPrivacyName: res.$t('global.text.dataProtection'), + schoolPrivacyLink: await getSchoolConsentVersionByType(req, res, 'privacy'), + schoolTermsLink: await getSchoolConsentVersionByType(req, res, 'termsOfUse'), + schoolPrivacyName: res.$t('global.text.dataProtectionFile'), + schoolTermsName: res.$t('global.text.termsOfUseFile'), invalid, secure, correctID, diff --git a/controllers/termsofuse.js b/controllers/termsofuse.js new file mode 100644 index 0000000000..4060a1435f --- /dev/null +++ b/controllers/termsofuse.js @@ -0,0 +1,33 @@ +const express = require('express'); +const { URL } = require('url'); +const { DOCUMENT_BASE_DIR, SC_THEME } = require('../config/global'); +const { specificFiles } = require('../config/documents'); +const { getBase64File } = require('../helpers/fileHelper'); +const { getConsentVersion } = require('../helpers/consentVersionHelper'); + +const router = express.Router(); + +const termsUrl = () => new URL(`${SC_THEME}/${specificFiles.termsOfUseSchool}`, DOCUMENT_BASE_DIR); + +router.get('/', async (req, res, next) => { + try { + const consentVersions = await getConsentVersion(req, res, 'termsOfUse'); + + if (consentVersions.data.length) { + const fileId = consentVersions.data[0].consentDataId; + if (!fileId) { + res.redirect(termsUrl().toString()); + } + + const fileTitle = res.$t('global.text.termsOfUseFile'); + + await getBase64File(req, res, fileId, fileTitle); + } else { + res.redirect(termsUrl().toString()); + } + } catch (err) { + next(err); + } +}); + +module.exports = router; diff --git a/helpers/consentVersionHelper.js b/helpers/consentVersionHelper.js new file mode 100644 index 0000000000..42683c07e4 --- /dev/null +++ b/helpers/consentVersionHelper.js @@ -0,0 +1,24 @@ +const api = require('../api'); +const authHelper = require('./authentication'); + +const getConsentVersion = async (req, res, consentType) => { + const isAuthenticated = await authHelper.isAuthenticated(req); + const qs = { + $limit: 1, + consentTypes: [consentType], + $sort: { + publishedAt: -1, + }, + }; + + if (isAuthenticated && res.locals.currentSchool) { + qs.schoolId = res.locals.currentSchool; + } + + const consentVersion = await api(req).get('/consentVersions', { qs }); + return consentVersion; +}; + +module.exports = { + getConsentVersion, +}; diff --git a/helpers/fileHelper.js b/helpers/fileHelper.js new file mode 100644 index 0000000000..924b2e49cb --- /dev/null +++ b/helpers/fileHelper.js @@ -0,0 +1,28 @@ +const api = require('../api'); + +const downloadAsPdf = (res, fileData, fileTitle) => { + // ERR_INVALID_CHAR will get thrown on ukrainian translation without encoding + const encodedFileTitle = encodeURI(fileTitle); + const download = Buffer.from(fileData, 'base64'); + res.writeHead(200, { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${encodedFileTitle}.pdf"`, + }).end(download); +}; + +const getBase64File = async (req, res, fileId, fileTitle) => { + if (fileId) { + const base64File = await api(req).get(`/base64Files/${fileId}`); + if (base64File.data) { + const fileData = base64File.data.replace( + 'data:application/pdf;base64,', + '', + ); + downloadAsPdf(res, fileData, fileTitle); + } + } +}; + +module.exports = { + getBase64File, +}; diff --git a/locales/de.json b/locales/de.json index ad20240a86..a921a0acce 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1458,7 +1458,6 @@ "i": "Ich,", "pleaseClickOnRead": "Bitte nimm mit Klick auf „Gelesen” diese Informationen zur Kenntnis, um mit der Nutzung fortzufahren.", "pleaseConfirmTheFollowingDeclarationOfConsent": "Bitte bestätige folgende Einwilligungserklärungen, damit du die {{title}} nutzen kannst.", - "termsOfUse": "Nutzungsordnung", "the": "die" } }, @@ -1771,6 +1770,8 @@ "dataProtectionThr": "Datenschutzhinweise", "dataProtectionFile": "Datenschutzerklärung der Schule", "dataProtectionFileThr": "Datenschutzhinweise der Schule", + "termsOfUse": "Nutzungsordnung", + "termsOfUseFile": "Nutzungsordnung der Schule", "emailDomainBlocked": "Dieser Mailprovider wird nicht mehr unterstützt. Bitte wende dich an deinen Schul-Admin.", "errorChangingFilePermissions": "Problem beim Ändern der Berechtigungen", "errorWhileLoadingPage": "Diese Seite konnte nicht geladen werden", @@ -2861,7 +2862,7 @@ }, "text": { "acceptConsentWithoutParents": "Wenn du zwischen 14 und {{age}} Jahre alt bist, bestätige bitte zusätzlich die Einverständniserklärung, damit du die {{title}} nutzen kannst.", - "agreeTermsOfUse": "Ich habe die Nutzungsordnung der {{title}} gelesen und stimme ihr zu.", + "agreeTermsOfUse": "Ich habe die Nutzungsordnung der {{title}} gelesen und stimme ihr zu.", "andAuthorisedToRepresent": "und berechtigt, den anderen Elternteil bei den nachfolgenden Erklärungen zu vertreten.", "iAgreeThatThepersonalData": "Ich erkläre mich damit einverstanden, dass die personenbezogenen Daten meines Kindes entsprechend der", "invalidLink": "Der Link ist leider nicht mehr gültig. Fordere einen neuen Link von deiner Lehrkraft oder dem Schul-Admin an.", diff --git a/locales/en.json b/locales/en.json index 75c53c9b11..4af8129adf 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1458,7 +1458,6 @@ "i": "I,", "pleaseClickOnRead": "Please acknowledge this information by clicking on „Read” in order to proceed with the use.", "pleaseConfirmTheFollowingDeclarationOfConsent": "Please confirm the following declarations of consent so that you can use the {{title}}.", - "termsOfUse": "Terms of Use", "the": "the" } }, @@ -1771,6 +1770,8 @@ "dataProtectionThr": "Privacy Policy", "dataProtectionFile": "Privacy Policy of School", "dataProtectionFileThr": "Privacy Policy of School", + "termsOfUse": "Terms of Use", + "termsOfUseFile": "Terms of Use of School", "emailDomainBlocked": "This mail provider is no longer supported. Please contact your school administrator.", "errorChangingFilePermissions": "Problem changing permissions", "errorWhileLoadingPage": "This page could not be loaded", @@ -2861,7 +2862,7 @@ }, "text": { "acceptConsentWithoutParents": "If you are between 14 and {{age}} years old, please also confirm the declaration of consent so that you can use the {{title}}.", - "agreeTermsOfUse": "I have read and agree to the Terms of Use of the {{title}}.", + "agreeTermsOfUse": "I have read and agree with the school's Terms of Use of the {{title}}.", "andAuthorisedToRepresent": "and entitled to represent the other parent in the following declarations.", "iAgreeThatThepersonalData": "I consent to my child's personal data being processed in accordance with the", "invalidLink": "Unfortunately the link is no longer valid. Request a new link from your teacher or school administrator.", diff --git a/locales/es.json b/locales/es.json index 96300336b1..4d3dc8d397 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1458,7 +1458,6 @@ "i": "Yo,", "pleaseClickOnRead": "Acepta esta información haciendo clic en \"Leer\" para continuar con el uso.", "pleaseConfirmTheFollowingDeclarationOfConsent": "Confirma las siguientes declaraciones de consentimiento para que puedas utilizar {{title}}.", - "termsOfUse": "Condiciones de uso", "the": "el" } }, @@ -1771,6 +1770,8 @@ "dataProtectionThr": "Política de Privacidad", "dataProtectionFile": "Política de Privacidad de la Escuela", "dataProtectionFileThr": "Política de Privacidad de la Escuela", + "termsOfUse": "Condiciones de Uso", + "termsOfUseFile": "Condiciones de Uso de la Escuela", "emailDomainBlocked": "Este proveedor de correo ya no es compatible. Ponte en contacto con el administrador de tu escuela.", "errorChangingFilePermissions": "Hay un problema para cambiar los permisos", "errorWhileLoadingPage": "Esta página no se ha podido cargar", @@ -2861,7 +2862,7 @@ }, "text": { "acceptConsentWithoutParents": "Si tienes entre 14 y {{age}} años, confirma también la declaración de consentimiento para que puedas utilizar {{title}}.", - "agreeTermsOfUse": "He leído y acepto las Condiciones de uso de {{title}}.", + "agreeTermsOfUse": "He leído y acepto las Condiciones de Uso de {{title}}.", "andAuthorisedToRepresent": "y con derecho a representar al otro padre en las siguientes declaraciones.", "iAgreeThatThepersonalData": "Doy mi consentimiento para que los datos personales de mi hijo sean procesados de acuerdo con la", "invalidLink": "Desafortunadamente el enlace ya no es válido. Solicita un nuevo enlace a tu profesor o al administrador de la escuela.", diff --git a/locales/uk.json b/locales/uk.json index 42c75d2931..859b3c1c5d 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -130,6 +130,8 @@ "dataProtectionThr": "Політика конфіденційності", "dataProtectionFile": "Політика конфіденційності школи", "dataProtectionFileThr": "Політика конфіденційності школи", + "termsOfUse": "Умови використання", + "termsOfUseFile": "Умови використання школи", "emailDomainBlocked": "Цей постачальник пошти більше не підтримується. Будь ласка, зв’яжіться з адміністратором школи.", "errorChangingFilePermissions": "Проблема зі зміною дозволів", "fileTooLarge": "Вкладені файли перевищують максимально дозволений розмір — {{ maxFileSizeInGb }} Гб!", @@ -2447,7 +2449,7 @@ "unknownError": "Ой, сталася невідома помилка. Спробуйте ще раз.", "welcomeMailSubject": "Ласкаво просимо до {{title}}!", "welcomeMailText": "Вітаємо, {{firstName}}!\n Ви можете ввійти до {{title}}, використовуючи такі дані для входу в систему:\n Адреса: {{address}}\n Електронна пошта: {{email}}\n {{password}}\n {{infotext}}\n Ваша\n команда {{shortTitle}} бажає вам веселощів та гарного початку", - "agreeTermsOfUse": "Я прочитав і погоджуюся з Умовами використання{{title}}.", + "agreeTermsOfUse": "Я прочитав і погоджуюся з Умовами використання школи {{title}}.", "invalidLink": "На жаль, посилання більше не дійсне. Попросіть нове посилання у свого викладача або адміністратора школи.", "invalidLinkEmbeddedWebsite": "Шкільна хмара була вбудована у веб-сайт. Щоби продовжити ", "invalidLinkOpenWebsite": "Відкрийте цю сторінку в окремому вікні" @@ -3039,7 +3041,6 @@ "i": "Я,", "pleaseClickOnRead": "Підтвердьте цю інформацію, натиснувши «Прочитати», щоб продовжити використання.", "pleaseConfirmTheFollowingDeclarationOfConsent": "Підтвердьте наведені нижче заяви про згоду, щоб ви могли використовувати {{title}}.", - "termsOfUse": "Умови використання", "the": "це" }, "headline": { diff --git a/theme/brb/views/lib/extended_footer.hbs b/theme/brb/views/lib/extended_footer.hbs index c81bfe073b..d960afabae 100644 --- a/theme/brb/views/lib/extended_footer.hbs +++ b/theme/brb/views/lib/extended_footer.hbs @@ -26,11 +26,11 @@

{{$t "lib.extended_footer.headline.links"}}