chore: Unify term types

This commit is contained in:
Maksim Eltyshev
2026-02-10 21:14:56 +01:00
parent e7326303cd
commit a82ae49fe4
14 changed files with 31 additions and 176 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ import http from './http';
/* Actions */ /* Actions */
const getTerms = (type, language, headers) => const getTerms = (language, headers) =>
http.get(`/terms/${type}${language ? `?language=${language}` : ''}`, undefined, headers); http.get(`/terms${language ? `?language=${language}` : ''}`, undefined, headers);
export default { export default {
getTerms, getTerms,
@@ -4,19 +4,15 @@
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Loader, Tab } from 'semantic-ui-react'; import { Loader, Tab } from 'semantic-ui-react';
import selectors from '../../../selectors';
import api from '../../../api'; import api from '../../../api';
import Markdown from '../Markdown'; import Markdown from '../Markdown';
import styles from './TermsPane.module.scss'; import styles from './TermsPane.module.scss';
const TermsPane = React.memo(() => { const TermsPane = React.memo(() => {
const type = useSelector((state) => selectors.selectCurrentUser(state).termsType);
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const [content, setContent] = useState(null); const [content, setContent] = useState(null);
@@ -24,7 +20,7 @@ const TermsPane = React.memo(() => {
async function fetchTerms() { async function fetchTerms() {
let terms; let terms;
try { try {
({ item: terms } = await api.getTerms(type, i18n.resolvedLanguage)); ({ item: terms } = await api.getTerms(i18n.resolvedLanguage));
} catch { } catch {
return; return;
} }
@@ -33,7 +29,7 @@ const TermsPane = React.memo(() => {
} }
fetchTerms(); fetchTerms();
}, [type, i18n.resolvedLanguage]); }, [i18n.resolvedLanguage]);
return ( return (
<Tab.Pane attached={false} className={styles.wrapper}> <Tab.Pane attached={false} className={styles.wrapper}>
+3 -9
View File
@@ -30,7 +30,7 @@ export function* authenticate(data) {
} catch (error) { } catch (error) {
let terms; let terms;
if (error.step === AccessTokenSteps.ACCEPT_TERMS) { if (error.step === AccessTokenSteps.ACCEPT_TERMS) {
({ item: terms } = yield call(api.getTerms, error.termsType, i18n.resolvedLanguage)); ({ item: terms } = yield call(api.getTerms, i18n.resolvedLanguage));
} }
yield put(actions.authenticate.failure(error, terms)); yield put(actions.authenticate.failure(error, terms));
@@ -129,7 +129,7 @@ export function* authenticateWithOidcCallback() {
} catch (error) { } catch (error) {
let terms; let terms;
if (error.step === AccessTokenSteps.ACCEPT_TERMS) { if (error.step === AccessTokenSteps.ACCEPT_TERMS) {
({ item: terms } = yield call(api.getTerms, error.termsType, i18n.resolvedLanguage)); ({ item: terms } = yield call(api.getTerms, i18n.resolvedLanguage));
} }
yield put(actions.authenticateWithOidc.failure(error, terms)); yield put(actions.authenticateWithOidc.failure(error, terms));
@@ -185,15 +185,9 @@ export function* cancelTerms() {
export function* updateTermsLanguage(value) { export function* updateTermsLanguage(value) {
yield put(actions.updateTermsLanguage(value)); yield put(actions.updateTermsLanguage(value));
const {
termsForm: {
payload: { type },
},
} = yield select(selectors.selectAuthenticateForm);
let terms; let terms;
try { try {
({ item: terms } = yield call(api.getTerms, type, value)); ({ item: terms } = yield call(api.getTerms, value));
} catch (error) { } catch (error) {
yield put(actions.updateTermsLanguage.failure(error)); yield put(actions.updateTermsLanguage.failure(error));
return; return;
@@ -31,7 +31,7 @@
* type: string * type: string
* minLength: 64 * minLength: 64
* maxLength: 64 * maxLength: 64
* description: Terms signature hash based on user role * description: Terms signature hash
* example: 940226c4c41f51afe3980ceb63704e752636526f4c52a4ea579e85b247493d94 * example: 940226c4c41f51afe3980ceb63704e752636526f4c52a4ea579e85b247493d94
* initialLanguage: * initialLanguage:
* type: string * type: string
@@ -184,14 +184,12 @@ module.exports = {
} }
if (!user.termsSignature) { if (!user.termsSignature) {
const termsSignature = sails.hooks.terms.getSignatureByUserRole(user.role); if (!sails.hooks.terms.isSignatureValid(inputs.signature)) {
if (inputs.signature !== termsSignature) {
throw Errors.INVALID_SIGNATURE; throw Errors.INVALID_SIGNATURE;
} }
const values = { const values = {
termsSignature, termsSignature: inputs.signature,
termsAcceptedAt: new Date().toISOString(), termsAcceptedAt: new Date().toISOString(),
}; };
+2 -20
View File
@@ -5,7 +5,7 @@
/** /**
* @swagger * @swagger
* /terms/{type}: * /terms:
* get: * get:
* summary: Get terms and conditions * summary: Get terms and conditions
* description: Retrieves terms and conditions in the specified language. * description: Retrieves terms and conditions in the specified language.
@@ -13,14 +13,6 @@
* - Terms * - Terms
* operationId: getTerms * operationId: getTerms
* parameters: * parameters:
* - name: type
* in: path
* required: true
* description: Type of terms to retrieve
* schema:
* type: string
* enum: [general, extended]
* example: general
* - name: language * - name: language
* in: query * in: query
* required: false * required: false
@@ -47,11 +39,6 @@
* - content * - content
* - signature * - signature
* properties: * properties:
* type:
* type: string
* enum: [general, extended]
* description: Type of terms
* example: general
* language: * language:
* type: string * type: string
* enum: [de-DE, en-US] * enum: [de-DE, en-US]
@@ -76,11 +63,6 @@
module.exports = { module.exports = {
inputs: { inputs: {
type: {
type: 'string',
isIn: Object.values(sails.hooks.terms.Types),
required: true,
},
language: { language: {
type: 'string', type: 'string',
isIn: User.LANGUAGES, isIn: User.LANGUAGES,
@@ -88,7 +70,7 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
const terms = await sails.hooks.terms.getPayload(inputs.type, inputs.language); const terms = await sails.hooks.terms.getPayload(inputs.language);
return { return {
item: terms, item: terms,
@@ -56,7 +56,7 @@ module.exports = {
} }
} }
if (!sails.hooks.terms.hasSignature(inputs.user.termsSignature)) { if (!sails.hooks.terms.isSignatureValid(inputs.user.termsSignature)) {
const { token: pendingToken, payload: pendingTokenPayload } = const { token: pendingToken, payload: pendingTokenPayload } =
sails.helpers.utils.createJwtToken( sails.helpers.utils.createJwtToken(
AccessTokenSteps.ACCEPT_TERMS, AccessTokenSteps.ACCEPT_TERMS,
@@ -82,12 +82,9 @@ module.exports = {
); );
} }
const termsType = sails.hooks.terms.getTypeByUserRole(inputs.user.role);
throw { throw {
termsAcceptanceRequired: { termsAcceptanceRequired: {
pendingToken, pendingToken,
termsType,
message: 'Terms acceptance required', message: 'Terms acceptance required',
step: AccessTokenSteps.ACCEPT_TERMS, step: AccessTokenSteps.ACCEPT_TERMS,
}, },
-1
View File
@@ -34,7 +34,6 @@ module.exports = {
}, },
}, },
language: inputs.record.language || sails.config.i18n.defaultLocale, language: inputs.record.language || sails.config.i18n.defaultLocale,
termsType: sails.hooks.terms.getTypeByUserRole(inputs.record.role),
}; };
const gravatarUrl = sails.helpers.users.buildGravatarUrl(inputs.record); const gravatarUrl = sails.helpers.users.buildGravatarUrl(inputs.record);
+13 -40
View File
@@ -14,22 +14,18 @@
const fsPromises = require('fs').promises; const fsPromises = require('fs').promises;
const crypto = require('crypto'); const crypto = require('crypto');
const Types = {
GENERAL: 'general',
EXTENDED: 'extended',
};
const LANGUAGES = ['de-DE', 'en-US']; const LANGUAGES = ['de-DE', 'en-US'];
const DEFAULT_LANGUAGE = 'en-US'; const DEFAULT_LANGUAGE = 'en-US';
const getContent = (language = DEFAULT_LANGUAGE) =>
fsPromises.readFile(`${sails.config.appPath}/terms/${language}.md`, 'utf8');
const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex'); const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
module.exports = function defineTermsHook(sails) { module.exports = function defineTermsHook(sails) {
let signatureByType; let signature;
let signaturesSet;
return { return {
Types,
LANGUAGES, LANGUAGES,
/** /**
@@ -39,49 +35,26 @@ module.exports = function defineTermsHook(sails) {
async initialize() { async initialize() {
sails.log.info('Initializing custom hook (`terms`)'); sails.log.info('Initializing custom hook (`terms`)');
signatureByType = { const content = await getContent();
[Types.GENERAL]: hashContent(await this.getContent(Types.GENERAL)), signature = hashContent(content);
[Types.EXTENDED]: hashContent(await this.getContent(Types.EXTENDED)),
};
signaturesSet = new Set(Object.values(signatureByType));
}, },
async getPayload(type, language = DEFAULT_LANGUAGE) { async getPayload(language = DEFAULT_LANGUAGE) {
if (!Object.values(Types).includes(type)) {
throw new Error(`Unknown type: ${type}`);
}
if (!LANGUAGES.includes(language)) { if (!LANGUAGES.includes(language)) {
language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign
} }
const content = await getContent(language);
return { return {
type,
language, language,
content: await this.getContent(type, language), content,
signature: this.getSignatureByType(type), signature,
}; };
}, },
getTypeByUserRole(userRole) { isSignatureValid(value) {
return userRole === User.Roles.ADMIN ? Types.EXTENDED : Types.GENERAL; return value === signature;
},
getContent(type, language = DEFAULT_LANGUAGE) {
return fsPromises.readFile(`${sails.config.appPath}/terms/${language}/${type}.md`, 'utf8');
},
getSignatureByType(type) {
return signatureByType[type];
},
getSignatureByUserRole(userRole) {
return signatureByType[this.getTypeByUserRole(userRole)];
},
hasSignature(signature) {
return signaturesSet.has(signature);
}, },
}; };
}; };
-6
View File
@@ -24,7 +24,6 @@
* - avatar * - avatar
* - phone * - phone
* - organization * - organization
* - termsType
* - isDeactivated * - isDeactivated
* - createdAt * - createdAt
* - updatedAt * - updatedAt
@@ -143,11 +142,6 @@
* default: byDefault * default: byDefault
* description: Default sort order for projects display (personal field) * description: Default sort order for projects display (personal field)
* example: byDefault * example: byDefault
* termsType:
* type: string
* enum: [general, extended]
* description: Type of terms applicable to the user based on role
* example: general
* isSsoUser: * isSsoUser:
* type: boolean * type: boolean
* default: false * default: false
+1 -1
View File
@@ -105,7 +105,7 @@ const protectedStaticDirServer = (prefix, getPathSegment) => (req, res, next) =>
module.exports.routes = { module.exports.routes = {
'GET /api/bootstrap': 'bootstrap/show', 'GET /api/bootstrap': 'bootstrap/show',
'GET /api/terms/:type': 'terms/show', 'GET /api/terms': 'terms/show',
'GET /api/config': 'config/show', 'GET /api/config': 'config/show',
'PATCH /api/config': 'config/update', 'PATCH /api/config': 'config/update',
@@ -1,4 +1,4 @@
# Beispiel-Nutzungsbedingungen - Allgemeine Benutzer # Beispiel-Nutzungsbedingungen
_Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._ _Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._
_Letzte Aktualisierung: 14. August 2025_ _Letzte Aktualisierung: 14. August 2025_
@@ -33,4 +33,4 @@ Bei Fragen zu diesen Beispiel-Bedingungen wenden Sie sich bitte an `placeholder@
--- ---
**Ende des Beispiels - Allgemeine Bedingungen** **Ende des Beispiels**
-39
View File
@@ -1,39 +0,0 @@
# Beispiel-Nutzungsbedingungen - Administratoren
_Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._
_Letzte Aktualisierung: 14. August 2025_
Willkommen, Administrator! Diese Beispiel-Nutzungsbedingungen ("Bedingungen") dienen **ausschließlich der Demonstration** und sind rechtlich nicht gültig. Diese erweiterte Version enthält zusätzliche Klauseln, die höhere Verantwortlichkeiten für Administratoren widerspiegeln.
---
## 1. Einführung
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed nibh id elit ultricies posuere.
## 2. Teilnahmeberechtigung
Administratoren müssen mindestens 21 Jahre alt und von der Organisation autorisiert sein. Nulla rhoncus diam non dictum fermentum.
## 3. Administrative Pflichten
Als Administrator stimmen Sie zu:
- Benutzerkonten verantwortungsvoll zu verwalten.
- Sicherheitseinstellungen korrekt zu konfigurieren.
- Die Einhaltung von Datenschutzvorschriften zu gewährleisten.
## 4. Verbotene Administrative Handlungen
Administratoren dürfen nicht:
1. Unbefugten Zugriff gewähren.
2. Prüfprotokolle ohne triftigen Grund ändern.
3. Administrative Rechte zum persönlichen Vorteil nutzen.
## 5. Datenverwaltung
Sie sind verantwortlich für den Schutz der Benutzerdaten und die Systemintegrität.
## 6. Änderungen
Diese Beispiel-Bedingungen können jederzeit zu Testzwecken geändert werden.
## 7. Kontakt
Bei Fragen zu diesen Beispiel-Bedingungen wenden Sie sich bitte an `placeholder@example.com`.
---
**Ende des Beispiels – Erweiterte (Admin) Bedingungen**
@@ -1,4 +1,4 @@
# Example Terms of Service - General Users # Example Terms of Service
_Not legally binding. This is placeholder text for testing purposes only._ _Not legally binding. This is placeholder text for testing purposes only._
_Last updated: August 14, 2025_ _Last updated: August 14, 2025_
@@ -33,4 +33,4 @@ For questions about these example Terms, please contact `placeholder@example.com
--- ---
**End of Example - General Terms** **End of Example**
-39
View File
@@ -1,39 +0,0 @@
# Example Terms of Service - Admin Users
_Not legally binding. This is placeholder text for testing purposes only._
_Last updated: August 14, 2025_
Welcome, Admin! These Example Terms of Service ("Terms") are **for demonstration purposes only** and are not legally valid. This extended version contains additional clauses reflecting higher responsibilities for administrative users.
---
## 1. Introduction
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed nibh id elit ultricies posuere.
## 2. Eligibility
Admins must be at least 21 years old and authorized by the organization. Nulla rhoncus diam non dictum fermentum.
## 3. Administrative Responsibilities
As an administrator, you agree to:
- Manage user accounts responsibly.
- Configure security settings accurately.
- Ensure compliance with data protection rules.
## 4. Prohibited Administrative Actions
Admins may not:
1. Grant unauthorized access.
2. Alter audit logs without proper reason.
3. Use administrative privileges for personal gain.
## 5. Data Management
You are responsible for safeguarding user data and system integrity.
## 6. Modifications
These example Terms may be updated for testing purposes without notice.
## 7. Contact
For questions about these example Terms, please contact `placeholder@example.com`.
---
**End of Example – Extended (Admin) Terms**