Merge branch 'feature/Consent_for_DPA_and_T_C' into develop
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -56,3 +56,5 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
||||
/nginx/.parcel-cache/
|
||||
/nginx/static/
|
||||
/docker/build_logs/
|
||||
/content/.Ulysses-Group.plist
|
||||
/content/.Ulysses-Settings.plist
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import date
|
||||
from enum import Enum
|
||||
|
||||
from common.extensions import db
|
||||
from flask_security import UserMixin, RoleMixin
|
||||
@@ -121,7 +122,6 @@ class User(db.Model, UserMixin):
|
||||
def has_roles(self, *args):
|
||||
return any(role.name in args for role in self.roles)
|
||||
|
||||
|
||||
class TenantDomain(db.Model):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
@@ -311,6 +311,49 @@ class PartnerTenant(db.Model):
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
|
||||
|
||||
class TenantConsent(db.Model):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||
partner_id = db.Column(db.Integer, db.ForeignKey('public.partner.id'), nullable=True)
|
||||
partner_service_id = db.Column(db.Integer, db.ForeignKey('public.partner_service.id'), nullable=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=False)
|
||||
consent_type = db.Column(db.String(50), nullable=False)
|
||||
consent_date = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
consent_version = db.Column(db.String(20), nullable=False, default="1.0.0")
|
||||
consent_data = db.Column(db.JSON, nullable=False)
|
||||
|
||||
# Tracking
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
|
||||
|
||||
class ConsentVersion(db.Model):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
consent_type = db.Column(db.String(50), nullable=False)
|
||||
consent_version = db.Column(db.String(20), nullable=False)
|
||||
consent_valid_from = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
consent_valid_to = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Tracking
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||
|
||||
|
||||
class ConsentStatus(str, Enum):
|
||||
CONSENTED = 'CONSENTED'
|
||||
NOT_CONSENTED = 'NOT_CONSENTED'
|
||||
RENEWAL_REQUIRED = 'RENEWAL_REQUIRED'
|
||||
CONSENT_EXPIRED = 'CONSENT_EXPIRED'
|
||||
UNKNOWN_CONSENT_VERSION = 'UNKNOWN_CONSENT_VERSION'
|
||||
|
||||
class SpecialistMagicLinkTenant(db.Model):
|
||||
__bind_key__ = 'public'
|
||||
__table_args__ = {'schema': 'public'}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from common.services.user.user_services import UserServices
|
||||
from common.services.user.partner_services import PartnerServices
|
||||
from common.services.user.tenant_services import TenantServices
|
||||
from common.services.user.consent_services import ConsentServices
|
||||
|
||||
__all__ = ['UserServices', 'PartnerServices', 'TenantServices']
|
||||
__all__ = ['UserServices', 'PartnerServices', 'TenantServices', 'ConsentServices']
|
||||
254
common/services/user/consent_services.py
Normal file
254
common/services/user/consent_services.py
Normal file
@@ -0,0 +1,254 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
|
||||
from flask import current_app, request, session
|
||||
from flask_security import current_user
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||
|
||||
from common.extensions import db
|
||||
from common.models.user import TenantConsent, ConsentVersion, ConsentStatus, PartnerService, PartnerTenant, Tenant
|
||||
|
||||
|
||||
@dataclass
|
||||
class TypeStatus:
|
||||
consent_type: str
|
||||
status: ConsentStatus
|
||||
active_version: Optional[str]
|
||||
last_version: Optional[str]
|
||||
|
||||
|
||||
class ConsentServices:
|
||||
@staticmethod
|
||||
def get_required_consent_types() -> List[str]:
|
||||
return list(current_app.config.get("CONSENT_TYPES", []))
|
||||
|
||||
@staticmethod
|
||||
def get_active_consent_version(consent_type: str) -> Optional[ConsentVersion]:
|
||||
try:
|
||||
# Active version: the one with consent_valid_to IS NULL, latest for this type
|
||||
return (ConsentVersion.query
|
||||
.filter_by(consent_type=consent_type, consent_valid_to=None)
|
||||
.order_by(desc(ConsentVersion.consent_valid_from))
|
||||
.first())
|
||||
except SQLAlchemyError as e:
|
||||
current_app.logger.error(f"DB error in get_active_consent_version({consent_type}): {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_tenant_last_consent(tenant_id: int, consent_type: str) -> Optional[TenantConsent]:
|
||||
try:
|
||||
return (TenantConsent.query
|
||||
.filter_by(tenant_id=tenant_id, consent_type=consent_type)
|
||||
.order_by(desc(TenantConsent.id))
|
||||
.first())
|
||||
except SQLAlchemyError as e:
|
||||
current_app.logger.error(f"DB error in get_tenant_last_consent({tenant_id}, {consent_type}): {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def evaluate_type_status(tenant_id: int, consent_type: str) -> TypeStatus:
|
||||
active = ConsentServices.get_active_consent_version(consent_type)
|
||||
if not active:
|
||||
current_app.logger.error(f"No active ConsentVersion found for type {consent_type}")
|
||||
return TypeStatus(consent_type, ConsentStatus.UNKNOWN_CONSENT_VERSION, None, None)
|
||||
|
||||
last = ConsentServices.get_tenant_last_consent(tenant_id, consent_type)
|
||||
if not last:
|
||||
return TypeStatus(consent_type, ConsentStatus.NOT_CONSENTED, active.consent_version, None)
|
||||
|
||||
# If last consent equals active → CONSENTED
|
||||
if last.consent_version == active.consent_version:
|
||||
return TypeStatus(consent_type, ConsentStatus.CONSENTED, active.consent_version, last.consent_version)
|
||||
|
||||
# Else: last refers to an older version; check its ConsentVersion to see grace period
|
||||
prev_cv = ConsentVersion.query.filter_by(consent_type=consent_type,
|
||||
consent_version=last.consent_version).first()
|
||||
if not prev_cv:
|
||||
current_app.logger.error(f"Tenant {tenant_id} references unknown ConsentVersion {last.consent_version} for {consent_type}")
|
||||
return TypeStatus(consent_type, ConsentStatus.UNKNOWN_CONSENT_VERSION, active.consent_version, last.consent_version)
|
||||
|
||||
if prev_cv.consent_valid_to:
|
||||
now = dt.now(tz.utc)
|
||||
if prev_cv.consent_valid_to >= now:
|
||||
# Within transition window
|
||||
return TypeStatus(consent_type, ConsentStatus.RENEWAL_REQUIRED, active.consent_version, last.consent_version)
|
||||
else:
|
||||
return TypeStatus(consent_type, ConsentStatus.NOT_CONSENTED, active.consent_version, last.consent_version)
|
||||
else:
|
||||
# Should not happen if a newer active exists; treat as unknown config
|
||||
current_app.logger.error(f"Previous ConsentVersion without valid_to while a newer active exists for {consent_type}")
|
||||
return TypeStatus(consent_type, ConsentStatus.UNKNOWN_CONSENT_VERSION, active.consent_version, last.consent_version)
|
||||
|
||||
@staticmethod
|
||||
def aggregate_status(type_statuses: List[TypeStatus]) -> ConsentStatus:
|
||||
# Priority: UNKNOWN > NOT_CONSENTED > RENEWAL_REQUIRED > CONSENTED
|
||||
priorities = {
|
||||
ConsentStatus.UNKNOWN_CONSENT_VERSION: 4,
|
||||
ConsentStatus.NOT_CONSENTED: 3,
|
||||
ConsentStatus.RENEWAL_REQUIRED: 2,
|
||||
ConsentStatus.CONSENTED: 1,
|
||||
}
|
||||
if not type_statuses:
|
||||
return ConsentStatus.CONSENTED
|
||||
worst = max(type_statuses, key=lambda ts: priorities.get(ts.status, 0))
|
||||
return worst.status
|
||||
|
||||
@staticmethod
|
||||
def get_consent_status(tenant_id: int) -> ConsentStatus:
|
||||
statuses = [ConsentServices.evaluate_type_status(tenant_id, ct) for ct in ConsentServices.get_required_consent_types()]
|
||||
return ConsentServices.aggregate_status(statuses)
|
||||
|
||||
@staticmethod
|
||||
def _is_tenant_admin_for(tenant_id: int) -> bool:
|
||||
try:
|
||||
return current_user.is_authenticated and current_user.has_roles('Tenant Admin') and getattr(current_user, 'tenant_id', None) == tenant_id
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_management_partner_for(tenant_id: int) -> Tuple[bool, Optional[int], Optional[int]]:
|
||||
"""Return (allowed, partner_id, partner_service_id) for management partner context."""
|
||||
try:
|
||||
if not (current_user.is_authenticated and current_user.has_roles('Partner Admin')):
|
||||
return False, None, None
|
||||
# Check PartnerTenant relationship via MANAGEMENT_SERVICE
|
||||
ps = PartnerService.query.filter_by(type='MANAGEMENT_SERVICE').all()
|
||||
if not ps:
|
||||
return False, None, None
|
||||
ps_ids = [p.id for p in ps]
|
||||
pt = PartnerTenant.query.filter_by(tenant_id=tenant_id).filter(PartnerTenant.partner_service_id.in_(ps_ids)).first()
|
||||
if not pt:
|
||||
return False, None, None
|
||||
the_ps = PartnerService.query.get(pt.partner_service_id)
|
||||
return True, the_ps.partner_id if the_ps else None, the_ps.id if the_ps else None
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error in _is_management_partner_for: {e}")
|
||||
return False, None, None
|
||||
|
||||
@staticmethod
|
||||
def can_consent_on_behalf(tenant_id: int) -> Tuple[bool, str, Optional[int], Optional[int]]:
|
||||
# Returns: allowed, mode('tenant_admin'|'management_partner'), partner_id, partner_service_id
|
||||
if ConsentServices._is_tenant_admin_for(tenant_id):
|
||||
return True, 'tenant_admin', None, None
|
||||
allowed, partner_id, partner_service_id = ConsentServices._is_management_partner_for(tenant_id)
|
||||
if allowed:
|
||||
return True, 'management_partner', partner_id, partner_service_id
|
||||
return False, 'none', None, None
|
||||
|
||||
@staticmethod
|
||||
def _resolve_consent_content(consent_type: str, version: str) -> Dict:
|
||||
"""Resolve canonical file ref and hash for a consent document.
|
||||
Uses configurable base dir, type subpaths, and patch-dir strategy.
|
||||
Defaults:
|
||||
- base: 'content'
|
||||
- map: {'Data Privacy Agreement':'dpa','Terms & Conditions':'terms'}
|
||||
- strategy: 'major_minor' -> a.b.c => a.b/a.b.c.md
|
||||
- ext: '.md'
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
cfg = current_app.config if current_app else {}
|
||||
base_dir = cfg.get('CONSENT_CONTENT_BASE_DIR', 'content')
|
||||
type_paths = cfg.get('CONSENT_TYPE_PATHS', {
|
||||
'Data Privacy Agreement': 'dpa',
|
||||
'Terms & Conditions': 'terms',
|
||||
})
|
||||
strategy = cfg.get('CONSENT_PATCH_DIR_STRATEGY', 'major_minor')
|
||||
ext = cfg.get('CONSENT_MARKDOWN_EXT', '.md')
|
||||
|
||||
type_dir = type_paths.get(consent_type, consent_type.lower().replace(' ', '_'))
|
||||
subpath = ''
|
||||
filename = f"{version}{ext}"
|
||||
try:
|
||||
parts = version.split('.')
|
||||
if strategy == 'major_minor' and len(parts) >= 2:
|
||||
subpath = f"{parts[0]}.{parts[1]}"
|
||||
filename = f"{parts[0]}.{parts[1]}.{parts[2] if len(parts)>2 else '0'}{ext}"
|
||||
# Build canonical path
|
||||
if subpath:
|
||||
canonical_ref = f"{base_dir}/{type_dir}/{subpath}/{filename}"
|
||||
else:
|
||||
canonical_ref = f"{base_dir}/{type_dir}/{filename}"
|
||||
except Exception:
|
||||
canonical_ref = f"{base_dir}/{type_dir}/{version}{ext}"
|
||||
|
||||
# Read file and hash
|
||||
content_hash = ''
|
||||
try:
|
||||
# project root = parent of app package
|
||||
root = Path(current_app.root_path).parent if current_app else Path('.')
|
||||
fpath = root / canonical_ref
|
||||
content_bytes = fpath.read_bytes() if fpath.exists() else b''
|
||||
content_hash = hashlib.sha256(content_bytes).hexdigest() if content_bytes else ''
|
||||
except Exception:
|
||||
content_hash = ''
|
||||
|
||||
return {
|
||||
'canonical_document_ref': canonical_ref,
|
||||
'content_hash': content_hash,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def record_consent(tenant_id: int, consent_type: str) -> TenantConsent:
|
||||
# Validate type
|
||||
if consent_type not in ConsentServices.get_required_consent_types():
|
||||
raise ValueError(f"Unknown consent type: {consent_type}")
|
||||
active = ConsentServices.get_active_consent_version(consent_type)
|
||||
if not active:
|
||||
raise RuntimeError(f"No active ConsentVersion for type {consent_type}")
|
||||
|
||||
allowed, mode, partner_id, partner_service_id = ConsentServices.can_consent_on_behalf(tenant_id)
|
||||
if not allowed:
|
||||
raise PermissionError("Not authorized to record consent for this tenant")
|
||||
|
||||
# Idempotency: if already consented for active version, return existing
|
||||
existing = (TenantConsent.query
|
||||
.filter_by(tenant_id=tenant_id, consent_type=consent_type, consent_version=active.consent_version)
|
||||
.first())
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
# Build consent_data with audit info
|
||||
ip = request.headers.get('X-Forwarded-For', '').split(',')[0].strip() or request.remote_addr or ''
|
||||
ua = request.headers.get('User-Agent', '')
|
||||
locale = session.get('locale') or request.accept_languages.best or ''
|
||||
content_meta = ConsentServices._resolve_consent_content(consent_type, active.consent_version)
|
||||
consent_data = {
|
||||
'source_ip': ip,
|
||||
'user_agent': ua,
|
||||
'locale': locale,
|
||||
**content_meta,
|
||||
}
|
||||
|
||||
tc = TenantConsent(
|
||||
tenant_id=tenant_id,
|
||||
partner_id=partner_id,
|
||||
partner_service_id=partner_service_id,
|
||||
user_id=getattr(current_user, 'id', None) or 0,
|
||||
consent_type=consent_type,
|
||||
consent_version=active.consent_version,
|
||||
consent_data=consent_data,
|
||||
)
|
||||
try:
|
||||
db.session.add(tc)
|
||||
db.session.commit()
|
||||
current_app.logger.info(f"Consent recorded: tenant={tenant_id}, type={consent_type}, version={active.consent_version}, mode={mode}, user={getattr(current_user, 'id', None)}")
|
||||
return tc
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
# In case of race, fetch existing
|
||||
current_app.logger.warning(f"IntegrityError on consent insert, falling back: {e}")
|
||||
existing = (TenantConsent.query
|
||||
.filter_by(tenant_id=tenant_id, consent_type=consent_type, consent_version=active.consent_version)
|
||||
.first())
|
||||
if existing:
|
||||
return existing
|
||||
raise
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"DB error in record_consent: {e}")
|
||||
raise
|
||||
@@ -6,7 +6,6 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from common.models.entitlements import PartnerServiceLicenseTier
|
||||
from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoSessionPartner
|
||||
|
||||
from common.utils.security_utils import current_user_has_role
|
||||
|
||||
|
||||
class PartnerServices:
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from flask import session, current_app
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.extensions import db, cache_manager
|
||||
from common.models.user import Partner, PartnerTenant, PartnerService, Tenant
|
||||
from common.models.user import Partner, PartnerTenant, PartnerService, Tenant, TenantConsent, ConsentStatus, \
|
||||
ConsentVersion
|
||||
from common.utils.eveai_exceptions import EveAINoManagementPartnerService
|
||||
from common.utils.model_logging_utils import set_logging_information
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
|
||||
from common.utils.security_utils import current_user_has_role
|
||||
|
||||
|
||||
class TenantServices:
|
||||
@@ -173,3 +174,9 @@ class TenantServices:
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error checking specialist type access: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_consent_status(tenant_id: int) -> ConsentStatus:
|
||||
# Delegate to centralized ConsentService to ensure consistent logic
|
||||
from common.services.user.consent_services import ConsentServices
|
||||
return ConsentServices.get_consent_status(tenant_id)
|
||||
|
||||
14
common/services/utils/version_services.py
Normal file
14
common/services/utils/version_services.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from flask import current_app
|
||||
|
||||
class VersionServices:
|
||||
@staticmethod
|
||||
def split_version(full_version: str) -> tuple[str, str]:
|
||||
parts = full_version.split(".")
|
||||
if len(parts) < 3:
|
||||
major_minor = '.'.join(parts[:2]) if len(parts) >= 2 else full_version
|
||||
patch = ''
|
||||
else:
|
||||
major_minor = '.'.join(parts[:2])
|
||||
patch = parts[2]
|
||||
|
||||
return major_minor, patch
|
||||
@@ -1,14 +1,39 @@
|
||||
from flask import request, url_for
|
||||
from flask import request, url_for, current_app
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
import re
|
||||
|
||||
VISIBLE_PREFIXES = ('/admin', '/api', '/chat-client')
|
||||
|
||||
|
||||
def _normalize_prefix(raw_prefix: str) -> str:
|
||||
"""Normalize config prefix to internal form '/admin' or '' if not set."""
|
||||
if not raw_prefix:
|
||||
return ''
|
||||
s = str(raw_prefix).strip()
|
||||
if not s:
|
||||
return ''
|
||||
# remove leading/trailing slashes, then add single leading slash
|
||||
s = s.strip('/')
|
||||
if not s:
|
||||
return ''
|
||||
return f"/{s}"
|
||||
|
||||
|
||||
def _get_config_prefix() -> str:
|
||||
"""Return normalized prefix from config EVEAI_APP_PREFIX (config-first)."""
|
||||
try:
|
||||
cfg_val = (current_app.config.get('EVEAI_APP_PREFIX') if current_app else None)
|
||||
return _normalize_prefix(cfg_val)
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def _derive_visible_prefix():
|
||||
# 1) Edge-provided header (beste en meest expliciete bron)
|
||||
xfp = request.headers.get('X-Forwarded-Prefix')
|
||||
if xfp and any(xfp.startswith(p) for p in VISIBLE_PREFIXES):
|
||||
return xfp.rstrip('/')
|
||||
current_app.logger.debug(f"X-Forwarded-Prefix: {xfp}")
|
||||
if xfp and any(str(xfp).startswith(p) for p in VISIBLE_PREFIXES):
|
||||
return str(xfp).rstrip('/')
|
||||
|
||||
# 2) Referer fallback: haal het top-level segment uit de Referer path
|
||||
ref = request.headers.get('Referer') or ''
|
||||
@@ -24,13 +49,31 @@ def _derive_visible_prefix():
|
||||
return ''
|
||||
|
||||
|
||||
def _visible_prefix_for_runtime() -> str:
|
||||
"""Decide which prefix to use at runtime.
|
||||
Priority: config EVEAI_APP_PREFIX; optional dynamic fallback if enabled.
|
||||
"""
|
||||
cfg_prefix = _get_config_prefix()
|
||||
if cfg_prefix:
|
||||
current_app.logger.debug(f"prefixed_url_for: using config prefix: {cfg_prefix}")
|
||||
return cfg_prefix
|
||||
# Optional dynamic fallback
|
||||
use_fallback = bool(current_app.config.get('EVEAI_USE_DYNAMIC_PREFIX_FALLBACK', False)) if current_app else False
|
||||
if use_fallback:
|
||||
dyn = _derive_visible_prefix()
|
||||
current_app.logger.debug(f"prefixed_url_for: using dynamic fallback prefix: {dyn}")
|
||||
return dyn
|
||||
current_app.logger.debug("prefixed_url_for: no prefix configured, no fallback enabled")
|
||||
return ''
|
||||
|
||||
|
||||
def prefixed_url_for(endpoint, **values):
|
||||
"""
|
||||
Gedrag:
|
||||
- Default (_external=False, for_redirect=False): retourneer relatief pad (zonder leading '/')
|
||||
voor templates/JS. De dynamische <base> zorgt voor correcte resolutie onder het zichtbare prefix.
|
||||
- _external=True: bouw absolute URL (schema/host). Als X-Forwarded-Prefix aanwezig is,
|
||||
prefixeer de path daarmee (handig voor e-mails/deeplinks).
|
||||
- _external=True: bouw absolute URL (schema/host). Pad wordt geprefixt met config prefix (indien gezet),
|
||||
of optioneel met dynamische fallback wanneer geactiveerd.
|
||||
- for_redirect=True: geef root-absoluut pad inclusief zichtbaar top-prefix, geschikt
|
||||
voor HTTP Location headers. Backwards compat: _as_location=True wordt behandeld als for_redirect.
|
||||
"""
|
||||
@@ -46,16 +89,20 @@ def prefixed_url_for(endpoint, **values):
|
||||
if external:
|
||||
scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
|
||||
host = request.headers.get('Host', request.host)
|
||||
xfp = request.headers.get('X-Forwarded-Prefix', '') or ''
|
||||
new_path = (xfp.rstrip('/') + path) if (xfp and not path.startswith(xfp)) else path
|
||||
visible_prefix = _visible_prefix_for_runtime()
|
||||
new_path = (visible_prefix.rstrip('/') + path) if (visible_prefix and not path.startswith(visible_prefix)) else path
|
||||
current_app.logger.debug(f"prefixed_url_for external: {scheme}://{host}{new_path}")
|
||||
return urlunsplit((scheme, host, new_path, query, fragment))
|
||||
|
||||
if for_redirect:
|
||||
visible_prefix = _derive_visible_prefix()
|
||||
visible_prefix = _visible_prefix_for_runtime()
|
||||
if visible_prefix and not path.startswith(visible_prefix):
|
||||
return f"{visible_prefix}{path}"
|
||||
# root-absoluut pad, zonder prefix als onbekend
|
||||
composed = f"{visible_prefix}{path}"
|
||||
current_app.logger.debug(f"prefixed_url_for redirect: {composed}")
|
||||
return composed
|
||||
current_app.logger.debug(f"prefixed_url_for redirect (no prefix): {path}")
|
||||
return path
|
||||
|
||||
# Default: relatief pad
|
||||
return path[1:] if path.startswith('/') else path
|
||||
# Default: relatief pad (zonder leading '/')
|
||||
rel = path[1:] if path.startswith('/') else path
|
||||
return rel
|
||||
@@ -6,6 +6,7 @@ from common.models.entitlements import License
|
||||
from common.utils.database import Database
|
||||
from common.utils.eveai_exceptions import EveAITenantNotFound, EveAITenantInvalid, EveAINoActiveLicense
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
from common.services.user import TenantServices
|
||||
|
||||
|
||||
# Definition of Trigger Handlers
|
||||
@@ -19,12 +20,15 @@ def set_tenant_session_data(sender, user, **kwargs):
|
||||
# Remove partner from session if it exists
|
||||
session.pop('partner', None)
|
||||
|
||||
session['consent_status'] = str(TenantServices.get_consent_status(user.tenant_id))
|
||||
|
||||
|
||||
def clear_tenant_session_data(sender, user, **kwargs):
|
||||
session.pop('tenant', None)
|
||||
session.pop('default_language', None)
|
||||
session.pop('default_llm_model', None)
|
||||
session.pop('partner', None)
|
||||
session.pop('consent_status', None)
|
||||
|
||||
|
||||
def is_valid_tenant(tenant_id):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from flask import current_app, render_template
|
||||
from flask import current_app, render_template, request, redirect, session, flash
|
||||
from flask_security import current_user
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
from common.models.user import Role
|
||||
from common.models.user import Role, ConsentStatus
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from common.utils.mail_utils import send_email
|
||||
|
||||
@@ -36,7 +36,7 @@ def send_confirmation_email(user):
|
||||
|
||||
try:
|
||||
send_email(user.email, f"{user.first_name} {user.last_name}", "Confirm your email", html)
|
||||
current_app.logger.info(f'Confirmation email sent to {user.email}')
|
||||
current_app.logger.info(f'Confirmation email sent to {user.email} with url: {confirm_url}')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Failed to send confirmation email to {user.email}. Error: {str(e)}')
|
||||
raise
|
||||
@@ -51,7 +51,7 @@ def send_reset_email(user):
|
||||
|
||||
try:
|
||||
send_email(user.email, f"{user.first_name} {user.last_name}", subject, html)
|
||||
current_app.logger.info(f'Reset email sent to {user.email}')
|
||||
current_app.logger.info(f'Reset email sent to {user.email} with url: {reset_url}')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Failed to send reset email to {user.email}. Error: {str(e)}')
|
||||
raise
|
||||
@@ -96,3 +96,108 @@ def current_user_roles():
|
||||
|
||||
def all_user_roles():
|
||||
roles = [(role.id, role.name) for role in Role.query.all()]
|
||||
|
||||
|
||||
def is_exempt_endpoint(endpoint: str) -> bool:
|
||||
"""Check if the endpoint is exempt from consent guard"""
|
||||
if not endpoint:
|
||||
return False
|
||||
cfg = current_app.config or {}
|
||||
endpoints_cfg = set(cfg.get('CONSENT_GUARD_EXEMPT_ENDPOINTS', []))
|
||||
prefix_cfg = list(cfg.get('CONSENT_GUARD_EXEMPT_PREFIXES', []))
|
||||
|
||||
default_endpoints = {
|
||||
'security_bp.login',
|
||||
'security_bp.logout',
|
||||
'security_bp.confirm_email',
|
||||
'security_bp.forgot_password',
|
||||
'security_bp.reset_password',
|
||||
'security_bp.reset_password_request',
|
||||
'user_bp.tenant_consent',
|
||||
'user_bp.no_consent',
|
||||
'user_bp.tenant_consent_renewal',
|
||||
'user_bp.consent_renewal',
|
||||
'user_bp.view_tenant_consents',
|
||||
'user_bp.accept_tenant_consent',
|
||||
'user_bp.view_consent_markdown',
|
||||
'basic_bp.view_content',
|
||||
}
|
||||
default_prefixes = [
|
||||
'security_bp.',
|
||||
'healthz_bp.',
|
||||
]
|
||||
endpoints = default_endpoints.union(endpoints_cfg)
|
||||
prefixes = default_prefixes + [p for p in prefix_cfg if isinstance(p, str)]
|
||||
for p in prefixes:
|
||||
if endpoint.startswith(p):
|
||||
return True
|
||||
if endpoint in endpoints:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def enforce_tenant_consent_ui():
|
||||
"""Check if the user has consented to the terms of service"""
|
||||
path = getattr(request, 'path', '') or ''
|
||||
if path.startswith('/healthz') or path.startswith('/_healthz'):
|
||||
current_app.logger.debug(f'Health check request, bypassing consent guard: {path}')
|
||||
return None
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
current_app.logger.debug('Not authenticated, bypassing consent guard')
|
||||
return None
|
||||
|
||||
endpoint = request.endpoint or ''
|
||||
if is_exempt_endpoint(endpoint) or request.method == 'OPTIONS':
|
||||
current_app.logger.debug(f'Endpoint exempt from consent guard: {endpoint}')
|
||||
return None
|
||||
|
||||
# Global bypass: Super User and Partner Admin always allowed
|
||||
if current_user.has_roles('Super User') or current_user.has_roles('Partner Admin'):
|
||||
current_app.logger.debug('Global bypass: Super User or Partner Admin')
|
||||
return None
|
||||
|
||||
tenant_id = getattr(current_user, 'tenant_id', None)
|
||||
if not tenant_id:
|
||||
tenant_id = session.get('tenant', {}).get('id') if session.get('tenant') else None
|
||||
if not tenant_id:
|
||||
return redirect(prefixed_url_for('security_bp.login', for_redirect=True))
|
||||
|
||||
raw_status = session.get('consent_status', ConsentStatus.NOT_CONSENTED)
|
||||
# Coerce string to ConsentStatus enum if needed
|
||||
status = raw_status
|
||||
try:
|
||||
if isinstance(raw_status, str):
|
||||
# Accept formats like 'CONSENTED' or 'ConsentStatus.CONSENTED'
|
||||
name = raw_status.split('.')[-1]
|
||||
from common.models.user import ConsentStatus as CS
|
||||
status = getattr(CS, name, CS.NOT_CONSENTED)
|
||||
except Exception:
|
||||
status = ConsentStatus.NOT_CONSENTED
|
||||
|
||||
if status == ConsentStatus.CONSENTED:
|
||||
current_app.logger.debug('User has consented')
|
||||
return None
|
||||
|
||||
if status == ConsentStatus.NOT_CONSENTED:
|
||||
current_app.logger.debug('User has not consented')
|
||||
if current_user.has_roles('Tenant Admin'):
|
||||
return redirect(prefixed_url_for('user_bp.tenant_consent', for_redirect=True))
|
||||
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||
if status == ConsentStatus.RENEWAL_REQUIRED:
|
||||
current_app.logger.debug('Consent renewal required')
|
||||
if current_user.has_roles('Tenant Admin'):
|
||||
flash(
|
||||
"You need to renew your consent to our DPA or T&Cs. Failing to do so in time will stop you from accessing our services.",
|
||||
"danger")
|
||||
elif current_user.has_roles('Partner Admin'):
|
||||
flash(
|
||||
"Please ensure renewal of our DPA or T&Cs for the current Tenant. Failing to do so in time will stop the tenant from accessing our services.",
|
||||
"danger")
|
||||
else:
|
||||
flash(
|
||||
"Please inform your administrator or partner to renew your consent to our DPA or T&Cs. Failing to do so in time will stop you from accessing our services.",
|
||||
"danger")
|
||||
return None
|
||||
current_app.logger.debug('Unknown consent status')
|
||||
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||
|
||||
@@ -310,15 +310,17 @@ class Config(object):
|
||||
# API Encryption ------------------------------------------------------------------------------
|
||||
API_ENCRYPTION_KEY = environ.get('API_ENCRYPTION_KEY')
|
||||
|
||||
# Email settings for API key notifications
|
||||
# Email settings for API key notifications ----------------------------------------------------
|
||||
PROMOTIONAL_IMAGE_URL = 'https://askeveai.com/wp-content/uploads/2024/07/Evie-Call-scaled.jpg' # Replace with your actual URL
|
||||
|
||||
# Langsmith settings
|
||||
LANGCHAIN_TRACING_V2 = True
|
||||
LANGCHAIN_ENDPOINT = 'https://api.smith.langchain.com'
|
||||
LANGCHAIN_PROJECT = "eveai"
|
||||
|
||||
# Type Definitions ----------------------------------------------------------------------------
|
||||
TENANT_TYPES = ['Active', 'Demo', 'Inactive', 'Test']
|
||||
CONSENT_TYPES = ["Data Privacy Agreement", "Terms & Conditions"]
|
||||
# CONSENT_TYPE_MAP maps names with the actual base folders the consent documents are stored in
|
||||
CONSENT_TYPE_MAP = {
|
||||
"Data Privacy Agreement": "dpa",
|
||||
"Terms & Conditions": "terms",
|
||||
}
|
||||
|
||||
# The maximum number of seconds allowed for audio compression (to save resources)
|
||||
MAX_COMPRESSION_DURATION = 60*10 # 10 minutes
|
||||
@@ -351,7 +353,7 @@ class Config(object):
|
||||
# Entitlement Constants
|
||||
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
||||
|
||||
# Content Directory for static content like the changelog, terms & conditions, privacy statement, ...
|
||||
# Content Directory for static content like the changelog, terms & conditions, dpa statement, ...
|
||||
CONTENT_DIR = '/app/content'
|
||||
|
||||
# Ensure health check endpoints are exempt from CSRF protection
|
||||
@@ -361,6 +363,22 @@ class Config(object):
|
||||
]
|
||||
SECURITY_LOGIN_WITHOUT_VIEWS = True # Dit voorkomt automatische redirects
|
||||
|
||||
# Define the nginx prefix used for the specific apps
|
||||
CHAT_CLIENT_PREFIX = 'chat-client/chat/'
|
||||
EVEAI_APP_PREFIX = 'admin/'
|
||||
# Whether to use dynamic fallback (X-Forwarded-Prefix/Referer) when EVEAI_APP_PREFIX is empty
|
||||
EVEAI_USE_DYNAMIC_PREFIX_FALLBACK = False
|
||||
|
||||
# Consent guard configuration (config-driven whitelist)
|
||||
# List of endpoint names to exempt from the global consent guard
|
||||
# Example: ['security_bp.login', 'security_bp.logout', 'user_bp.tenant_consent']
|
||||
CONSENT_GUARD_EXEMPT_ENDPOINTS = []
|
||||
# List of endpoint name prefixes; any endpoint starting with one of these is exempt
|
||||
# Example: ['security_bp.', 'healthz_bp.']
|
||||
CONSENT_GUARD_EXEMPT_PREFIXES = []
|
||||
# TTL for consent status stored in session (seconds)
|
||||
CONSENT_SESSION_TTL_SECONDS = int(environ.get('CONSENT_SESSION_TTL_SECONDS', '45'))
|
||||
|
||||
|
||||
class DevConfig(Config):
|
||||
DEVELOPMENT = True
|
||||
@@ -368,9 +386,6 @@ class DevConfig(Config):
|
||||
FLASK_DEBUG = True
|
||||
EXPLAIN_TEMPLATE_LOADING = False
|
||||
|
||||
# Define the nginx prefix used for the specific apps
|
||||
CHAT_CLIENT_PREFIX = 'chat-client/chat/'
|
||||
|
||||
# Define the static path
|
||||
STATIC_URL = None
|
||||
|
||||
@@ -394,9 +409,6 @@ class TestConfig(Config):
|
||||
FLASK_DEBUG = True
|
||||
EXPLAIN_TEMPLATE_LOADING = False
|
||||
|
||||
# Define the nginx prefix used for the specific apps
|
||||
CHAT_CLIENT_PREFIX = 'chat-client/chat/'
|
||||
|
||||
# Define the static path
|
||||
STATIC_URL = None
|
||||
|
||||
@@ -420,9 +432,6 @@ class StagingConfig(Config):
|
||||
FLASK_DEBUG = True
|
||||
EXPLAIN_TEMPLATE_LOADING = False
|
||||
|
||||
# Define the nginx prefix used for the specific apps
|
||||
CHAT_CLIENT_PREFIX = 'chat-client/chat/'
|
||||
|
||||
# Define the static path
|
||||
STATIC_URL = 'https://evie-staging-static.askeveai.com/'
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ fields:
|
||||
required: true
|
||||
meta:
|
||||
kind: "consent"
|
||||
consentRich: "Ik Agree with the <terms>Terms and Conditions</terms> and the <privacy>Privacy Statement</privacy> of Ask Eve AI"
|
||||
consentRich: "Ik Agree with the <terms>Terms and Conditions</terms> and the <dpa>Privacy Statement</dpa> of Ask Eve AI"
|
||||
ariaPrivacy: "Open privacyverklaring in a modal dialog"
|
||||
ariaTerms: "Open algemene voorwaarden in a modal dialog"
|
||||
metadata:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dist/chat-client.js": "dist/chat-client.421bb8ee.js",
|
||||
"dist/chat-client.css": "dist/chat-client.23ac6be5.css",
|
||||
"dist/main.js": "dist/main.f3dde0f6.js",
|
||||
"dist/main.css": "dist/main.c40e57ad.css"
|
||||
"dist/chat-client.js": "dist/chat-client.59b28883.js",
|
||||
"dist/chat-client.css": "dist/chat-client.79757200.css",
|
||||
"dist/main.js": "dist/main.c5b0c81d.js",
|
||||
"dist/main.css": "dist/main.06893f70.css"
|
||||
}
|
||||
671
content/DPIA template/1.0.0.md
Normal file
671
content/DPIA template/1.0.0.md
Normal file
@@ -0,0 +1,671 @@
|
||||
# Data Protection Impact Assessment (DPIA) Template
|
||||
## Ask Eve AI
|
||||
|
||||
**Date of Assessment**: [Date]
|
||||
**Assessed By**: [Name, Role]
|
||||
**Review Date**: [Date - recommend annual review]
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
| Field | Details |
|
||||
|-------|---------|
|
||||
| **Processing Activity Name** | [e.g., "Job Candidate Assessment Specialist"] |
|
||||
| **Brief Description** | [1-2 sentence summary] |
|
||||
| **Risk Level** | ☐ Low ☐ Medium ☐ High |
|
||||
| **DPIA Required?** | ☐ Yes ☐ No |
|
||||
| **Status** | ☐ Draft ☐ Under Review ☐ Approved ☐ Requires Revision |
|
||||
|
||||
---
|
||||
|
||||
## 2. Description of the Processing
|
||||
|
||||
### 2.1 Nature of the Processing
|
||||
|
||||
**What Personal Data will be processed?**
|
||||
- [ ] Contact information (name, email, phone)
|
||||
- [ ] Identification data (ID numbers, passport)
|
||||
- [ ] Professional data (CV, work history, qualifications)
|
||||
- [ ] Assessment results or scores
|
||||
- [ ] Communication records
|
||||
- [ ] Behavioral data (how users interact with the system)
|
||||
- [ ] Technical data (IP addresses, device information)
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Categories of Data Subjects:**
|
||||
- [ ] Job applicants/candidates
|
||||
- [ ] Employees
|
||||
- [ ] Customers
|
||||
- [ ] End users/consumers
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Volume of Data Subjects:**
|
||||
- [ ] < 100
|
||||
- [ ] 100-1,000
|
||||
- [ ] 1,000-10,000
|
||||
- [ ] > 10,000
|
||||
|
||||
### 2.2 Scope of the Processing
|
||||
|
||||
**What is the purpose of the processing?**
|
||||
|
||||
[Describe the specific business purpose, e.g., "To assess job candidates' suitability for specific roles by analyzing their responses to standardized questions"]
|
||||
|
||||
**How will the data be collected?**
|
||||
- [ ] Directly from data subjects (forms, interviews)
|
||||
- [ ] From third parties (recruiters, references)
|
||||
- [ ] Automated collection (web forms, chatbots)
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Where will data be stored?**
|
||||
- [ ] EU (specify: France - Scaleway)
|
||||
- [ ] Non-EU (specify and justify): _______________
|
||||
|
||||
### 2.3 Context of the Processing
|
||||
|
||||
**Is this processing new or existing?**
|
||||
- [ ] New processing activity
|
||||
- [ ] Modification of existing processing
|
||||
- [ ] Existing processing (periodic review)
|
||||
|
||||
**Who has access to the Personal Data?**
|
||||
- [ ] Ask Eve AI employees (specify roles): _______________
|
||||
- [ ] Customer/Tenant employees
|
||||
- [ ] Partners (specify): _______________
|
||||
- [ ] Sub-Processors (list): _______________
|
||||
- [ ] Other: _______________
|
||||
|
||||
**How long will data be retained?**
|
||||
|
||||
[Specify retention period and justification, e.g., "Candidate data retained for 12 months to comply with recruitment record-keeping requirements"]
|
||||
|
||||
---
|
||||
|
||||
## 3. Necessity and Proportionality Assessment
|
||||
|
||||
### 3.1 Lawful Basis
|
||||
|
||||
**What is the lawful basis for processing? (Article 6 GDPR)**
|
||||
- [ ] **Consent** - Data subject has given explicit consent
|
||||
- [ ] **Contract** - Processing necessary for contract performance
|
||||
- [ ] **Legal obligation** - Required by law
|
||||
- [ ] **Vital interests** - Necessary to protect someone's life
|
||||
- [ ] **Public task** - Performing a public interest task
|
||||
- [ ] **Legitimate interests** - Necessary for legitimate interests (requires balancing test)
|
||||
|
||||
**Justification:**
|
||||
|
||||
[Explain why this lawful basis applies]
|
||||
|
||||
### 3.2 Special Categories of Data (if applicable)
|
||||
|
||||
**Does the processing involve special categories of data? (Article 9 GDPR)**
|
||||
- [ ] No
|
||||
- [ ] Yes - racial or ethnic origin
|
||||
- [ ] Yes - political opinions
|
||||
- [ ] Yes - religious or philosophical beliefs
|
||||
- [ ] Yes - trade union membership
|
||||
- [ ] Yes - genetic data
|
||||
- [ ] Yes - biometric data for identification
|
||||
- [ ] Yes - health data
|
||||
- [ ] Yes - sex life or sexual orientation data
|
||||
|
||||
**If yes, what is the additional lawful basis?**
|
||||
|
||||
[Article 9(2) provides specific conditions - specify which applies]
|
||||
|
||||
### 3.3 Automated Decision-Making
|
||||
|
||||
**Does the processing involve automated decision-making or profiling?**
|
||||
- [ ] No
|
||||
- [ ] Yes - automated decision-making WITH human oversight
|
||||
- [ ] Yes - fully automated decision-making (no human intervention)
|
||||
|
||||
**If yes:**
|
||||
|
||||
**Does it produce legal effects or similarly significant effects?**
|
||||
- [ ] No
|
||||
- [ ] Yes (explain): _______________
|
||||
|
||||
**What safeguards are in place?**
|
||||
- [ ] Right to obtain human intervention
|
||||
- [ ] Right to express point of view
|
||||
- [ ] Right to contest the decision
|
||||
- [ ] Regular accuracy reviews
|
||||
- [ ] Transparency about logic involved
|
||||
- [ ] Other: _______________
|
||||
|
||||
### 3.4 Necessity Test
|
||||
|
||||
**Is the processing necessary to achieve the stated purpose?**
|
||||
|
||||
☐ Yes ☐ No
|
||||
|
||||
**Justification:**
|
||||
|
||||
[Explain why this specific processing is necessary and whether less intrusive alternatives were considered]
|
||||
|
||||
**Could the purpose be achieved with less data or through other means?**
|
||||
|
||||
☐ Yes (explain why not pursued): _______________
|
||||
☐ No
|
||||
|
||||
### 3.5 Proportionality Test
|
||||
|
||||
**Is the processing proportionate to the purpose?**
|
||||
|
||||
☐ Yes ☐ No
|
||||
|
||||
**Data Minimization:**
|
||||
- Are you collecting only the minimum data necessary? ☐ Yes ☐ No
|
||||
- Have you considered pseudonymization or anonymization? ☐ Yes ☐ No ☐ N/A
|
||||
- Can data be aggregated instead of individual records? ☐ Yes ☐ No ☐ N/A
|
||||
|
||||
**Storage Limitation:**
|
||||
- Is the retention period justified and documented? ☐ Yes ☐ No
|
||||
- Is there an automated deletion process? ☐ Yes ☐ No ☐ Planned
|
||||
|
||||
---
|
||||
|
||||
## 4. Stakeholder Consultation
|
||||
|
||||
### 4.1 Data Subject Consultation
|
||||
|
||||
**Have data subjects been consulted about this processing?**
|
||||
|
||||
☐ Yes ☐ No ☐ Not required
|
||||
|
||||
**If yes, how were they consulted?**
|
||||
|
||||
[Describe consultation method: surveys, focus groups, user research, etc.]
|
||||
|
||||
**Key concerns raised by data subjects:**
|
||||
|
||||
[List any concerns and how they were addressed]
|
||||
|
||||
### 4.2 DPO or Security Contact Consultation
|
||||
|
||||
**Has the DPO or security contact been consulted?**
|
||||
|
||||
☐ Yes ☐ No ☐ N/A (no formal DPO)
|
||||
|
||||
**Comments from DPO/Security Contact:**
|
||||
|
||||
[Record any recommendations or concerns]
|
||||
|
||||
---
|
||||
|
||||
## 5. Risk Assessment
|
||||
|
||||
### 5.1 Risk Identification
|
||||
|
||||
For each risk, assess:
|
||||
- **Likelihood**: Negligible / Low / Medium / High
|
||||
- **Severity**: Negligible / Low / Medium / High
|
||||
- **Overall Risk**: Low / Medium / High / Very High
|
||||
|
||||
**Risk 1: Unauthorized Access or Data Breach**
|
||||
|
||||
**Description**: Personal data could be accessed by unauthorized parties due to security vulnerabilities.
|
||||
|
||||
| Assessment | Rating |
|
||||
|------------|--------|
|
||||
| Likelihood | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| Severity (if occurs) | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| **Overall Risk** | ☐ Low ☐ Medium ☐ High ☐ Very High |
|
||||
|
||||
**Risk 2: Discrimination or Bias in Automated Decisions**
|
||||
|
||||
**Description**: Automated processing could result in discriminatory outcomes or unfair treatment.
|
||||
|
||||
| Assessment | Rating |
|
||||
|------------|--------|
|
||||
| Likelihood | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| Severity (if occurs) | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| **Overall Risk** | ☐ Low ☐ Medium ☐ High ☐ Very High |
|
||||
|
||||
**Risk 3: Lack of Transparency**
|
||||
|
||||
**Description**: Data subjects may not understand how their data is processed or decisions are made.
|
||||
|
||||
| Assessment | Rating |
|
||||
|------------|--------|
|
||||
| Likelihood | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| Severity (if occurs) | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| **Overall Risk** | ☐ Low ☐ Medium ☐ High ☐ Very High |
|
||||
|
||||
**Risk 4: Inability to Exercise Data Subject Rights**
|
||||
|
||||
**Description**: Data subjects may have difficulty exercising their rights (access, erasure, portability, etc.).
|
||||
|
||||
| Assessment | Rating |
|
||||
|------------|--------|
|
||||
| Likelihood | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| Severity (if occurs) | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| **Overall Risk** | ☐ Low ☐ Medium ☐ High ☐ Very High |
|
||||
|
||||
**Risk 5: Data Quality Issues**
|
||||
|
||||
**Description**: Inaccurate or outdated data could lead to incorrect decisions or outcomes.
|
||||
|
||||
| Assessment | Rating |
|
||||
|------------|--------|
|
||||
| Likelihood | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| Severity (if occurs) | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| **Overall Risk** | ☐ Low ☐ Medium ☐ High ☐ Very High |
|
||||
|
||||
**Risk 6: Function Creep / Scope Expansion**
|
||||
|
||||
**Description**: Data collected for one purpose could be used for other purposes without consent.
|
||||
|
||||
| Assessment | Rating |
|
||||
|------------|--------|
|
||||
| Likelihood | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| Severity (if occurs) | ☐ Negligible ☐ Low ☐ Medium ☐ High |
|
||||
| **Overall Risk** | ☐ Low ☐ Medium ☐ High ☐ Very High |
|
||||
|
||||
**Additional Risks:**
|
||||
|
||||
[Add any processing-specific risks]
|
||||
|
||||
---
|
||||
|
||||
## 6. Mitigation Measures
|
||||
|
||||
For each identified risk, document mitigation measures:
|
||||
|
||||
### Risk 1: Unauthorized Access or Data Breach
|
||||
|
||||
**Mitigation Measures:**
|
||||
- [ ] Encryption in transit (TLS 1.2+)
|
||||
- [ ] Encryption at rest
|
||||
- [ ] Multi-factor authentication
|
||||
- [ ] Access controls (RBAC)
|
||||
- [ ] Regular security audits
|
||||
- [ ] WAF and DDoS protection (Bunny.net Shield)
|
||||
- [ ] Multi-tenant data isolation
|
||||
- [ ] Regular security training
|
||||
- [ ] Incident response plan
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Residual Risk After Mitigation:** ☐ Low ☐ Medium ☐ High ☐ Very High
|
||||
|
||||
### Risk 2: Discrimination or Bias in Automated Decisions
|
||||
|
||||
**Mitigation Measures:**
|
||||
- [ ] Regular bias testing of AI models
|
||||
- [ ] Diverse training data sets
|
||||
- [ ] Human review of automated decisions
|
||||
- [ ] Clear criteria for decision-making
|
||||
- [ ] Right to contest decisions
|
||||
- [ ] Transparency about decision logic
|
||||
- [ ] Regular fairness audits
|
||||
- [ ] Monitoring of outcomes by demographic groups
|
||||
- [ ] Ability to request explanation
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Residual Risk After Mitigation:** ☐ Low ☐ Medium ☐ High ☐ Very High
|
||||
|
||||
### Risk 3: Lack of Transparency
|
||||
|
||||
**Mitigation Measures:**
|
||||
- [ ] Clear Privacy Policy explaining processing
|
||||
- [ ] Explicit consent mechanisms
|
||||
- [ ] Plain language explanations
|
||||
- [ ] Information provided before data collection
|
||||
- [ ] Explanation of automated decision logic
|
||||
- [ ] Contact information for questions
|
||||
- [ ] Regular communication with data subjects
|
||||
- [ ] Privacy-by-design approach (anonymous until consent)
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Residual Risk After Mitigation:** ☐ Low ☐ Medium ☐ High ☐ Very High
|
||||
|
||||
### Risk 4: Inability to Exercise Data Subject Rights
|
||||
|
||||
**Mitigation Measures:**
|
||||
- [ ] Clear procedures for rights requests
|
||||
- [ ] Multiple request channels (email, helpdesk)
|
||||
- [ ] 30-day response timeframe
|
||||
- [ ] Technical capability to extract data
|
||||
- [ ] Data portability in standard formats
|
||||
- [ ] Secure deletion processes
|
||||
- [ ] Account disabling/restriction capability
|
||||
- [ ] Identity verification procedures
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Residual Risk After Mitigation:** ☐ Low ☐ Medium ☐ High ☐ Very High
|
||||
|
||||
### Risk 5: Data Quality Issues
|
||||
|
||||
**Mitigation Measures:**
|
||||
- [ ] Data validation on input
|
||||
- [ ] Regular data accuracy reviews
|
||||
- [ ] Ability for data subjects to correct errors
|
||||
- [ ] Clear data update procedures
|
||||
- [ ] Data quality monitoring
|
||||
- [ ] Source verification for third-party data
|
||||
- [ ] Archiving of outdated data
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Residual Risk After Mitigation:** ☐ Low ☐ Medium ☐ High ☐ Very High
|
||||
|
||||
### Risk 6: Function Creep / Scope Expansion
|
||||
|
||||
**Mitigation Measures:**
|
||||
- [ ] Documented purpose limitation
|
||||
- [ ] Access controls preventing unauthorized use
|
||||
- [ ] Regular compliance audits
|
||||
- [ ] Privacy Policy clearly states purposes
|
||||
- [ ] Consent required for new purposes
|
||||
- [ ] Technical controls preventing misuse
|
||||
- [ ] Staff training on data protection
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Residual Risk After Mitigation:** ☐ Low ☐ Medium ☐ High ☐ Very High
|
||||
|
||||
### Additional Mitigation Measures
|
||||
|
||||
[Document any additional mitigation measures not covered above]
|
||||
|
||||
---
|
||||
|
||||
## 7. Data Subject Rights Implementation
|
||||
|
||||
**How will you ensure data subjects can exercise their rights?**
|
||||
|
||||
### Right of Access (Article 15)
|
||||
- [ ] Procedure documented
|
||||
- [ ] Technical capability implemented
|
||||
- [ ] Response within 30 days
|
||||
- Method: _______________
|
||||
|
||||
### Right to Rectification (Article 16)
|
||||
- [ ] Procedure documented
|
||||
- [ ] Technical capability implemented
|
||||
- [ ] Response within 30 days
|
||||
- Method: _______________
|
||||
|
||||
### Right to Erasure (Article 17)
|
||||
- [ ] Procedure documented
|
||||
- [ ] Technical capability implemented
|
||||
- [ ] Response within 30 days
|
||||
- Method: _______________
|
||||
- Limitations: _______________
|
||||
|
||||
### Right to Restriction (Article 18)
|
||||
- [ ] Procedure documented
|
||||
- [ ] Technical capability implemented (account disabling)
|
||||
- [ ] Response within 30 days
|
||||
|
||||
### Right to Data Portability (Article 20)
|
||||
- [ ] Procedure documented
|
||||
- [ ] Technical capability implemented
|
||||
- [ ] Export format: JSON / CSV / XML / Other: _______________
|
||||
|
||||
### Right to Object (Article 21)
|
||||
- [ ] Procedure documented
|
||||
- [ ] Opt-out mechanisms implemented
|
||||
- [ ] Clear in Privacy Policy
|
||||
|
||||
### Rights Related to Automated Decision-Making (Article 22)
|
||||
- [ ] Human intervention available
|
||||
- [ ] Explanation of logic provided
|
||||
- [ ] Right to contest implemented
|
||||
- [ ] Documented in Privacy Policy
|
||||
|
||||
---
|
||||
|
||||
## 8. Privacy by Design and Default
|
||||
|
||||
**Privacy Enhancing Technologies Implemented:**
|
||||
- [ ] Data minimization (collect only necessary data)
|
||||
- [ ] Pseudonymization (where applicable)
|
||||
- [ ] Anonymization (where applicable)
|
||||
- [ ] Anonymous interaction until consent (privacy-by-design)
|
||||
- [ ] Encryption (in transit and at rest)
|
||||
- [ ] Access controls and authentication
|
||||
- [ ] Audit logging
|
||||
- [ ] Secure deletion
|
||||
- [ ] Data isolation (multi-tenant architecture)
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Default Settings:**
|
||||
- [ ] Most privacy-protective settings by default
|
||||
- [ ] Opt-in (not opt-out) for non-essential processing
|
||||
- [ ] Clear consent mechanisms before data collection
|
||||
- [ ] Limited data sharing by default
|
||||
|
||||
---
|
||||
|
||||
## 9. Compliance with Principles
|
||||
|
||||
**For each GDPR principle, confirm compliance:**
|
||||
|
||||
### Lawfulness, Fairness, Transparency (Article 5(1)(a))
|
||||
- [ ] Lawful basis identified and documented
|
||||
- [ ] Processing is fair and transparent
|
||||
- [ ] Privacy Policy clearly explains processing
|
||||
- Evidence: _______________
|
||||
|
||||
### Purpose Limitation (Article 5(1)(b))
|
||||
- [ ] Specific purposes documented
|
||||
- [ ] Data not used for incompatible purposes
|
||||
- [ ] New purposes require new consent/legal basis
|
||||
- Evidence: _______________
|
||||
|
||||
### Data Minimization (Article 5(1)(c))
|
||||
- [ ] Only necessary data collected
|
||||
- [ ] Regular review of data collected
|
||||
- [ ] Excess data not retained
|
||||
- Evidence: _______________
|
||||
|
||||
### Accuracy (Article 5(1)(d))
|
||||
- [ ] Mechanisms to ensure data accuracy
|
||||
- [ ] Ability to correct inaccurate data
|
||||
- [ ] Regular data quality reviews
|
||||
- Evidence: _______________
|
||||
|
||||
### Storage Limitation (Article 5(1)(e))
|
||||
- [ ] Retention periods defined and documented
|
||||
- [ ] Automated deletion where appropriate
|
||||
- [ ] Justification for retention documented
|
||||
- Evidence: _______________
|
||||
|
||||
### Integrity and Confidentiality (Article 5(1)(f))
|
||||
- [ ] Appropriate security measures implemented
|
||||
- [ ] Protection against unauthorized access
|
||||
- [ ] Encryption and access controls in place
|
||||
- Evidence: See Annex 2 of DPA
|
||||
|
||||
### Accountability (Article 5(2))
|
||||
- [ ] Documentation of compliance measures
|
||||
- [ ] Records of processing activities maintained
|
||||
- [ ] DPIA conducted and documented
|
||||
- [ ] DPA in place with processors
|
||||
- Evidence: This DPIA, DPA with customers
|
||||
|
||||
---
|
||||
|
||||
## 10. International Transfers
|
||||
|
||||
**Does this processing involve transfer to third countries?**
|
||||
|
||||
☐ No - all processing within EU
|
||||
☐ Yes (complete below)
|
||||
|
||||
**If yes:**
|
||||
|
||||
**Country/Region:** _______________
|
||||
|
||||
**Transfer Mechanism:**
|
||||
- [ ] Adequacy decision (Article 45)
|
||||
- [ ] Standard Contractual Clauses (Article 46)
|
||||
- [ ] Binding Corporate Rules (Article 47)
|
||||
- [ ] Other: _______________
|
||||
|
||||
**Transfer Impact Assessment Completed?** ☐ Yes ☐ No
|
||||
|
||||
**Additional Safeguards:**
|
||||
|
||||
[Document supplementary measures to ensure adequate protection]
|
||||
|
||||
---
|
||||
|
||||
## 11. Documentation and Records
|
||||
|
||||
**Documentation Maintained:**
|
||||
- [ ] This DPIA
|
||||
- [ ] Privacy Policy
|
||||
- [ ] Data Processing Agreement
|
||||
- [ ] Consent records (if applicable)
|
||||
- [ ] Records of processing activities (Article 30)
|
||||
- [ ] Data breach register
|
||||
- [ ] Data Subject rights request log
|
||||
- [ ] Staff training records
|
||||
- [ ] Sub-processor agreements
|
||||
|
||||
**Record of Processing Activities (Article 30) Completed?**
|
||||
|
||||
☐ Yes ☐ No ☐ In Progress
|
||||
|
||||
---
|
||||
|
||||
## 12. Outcomes and Recommendations
|
||||
|
||||
### 12.1 Overall Risk Assessment
|
||||
|
||||
**After implementing mitigation measures, what is the residual risk level?**
|
||||
|
||||
☐ Low - processing can proceed
|
||||
☐ Medium - additional measures recommended
|
||||
☐ High - significant concerns, consult DPO/legal counsel
|
||||
☐ Very High - processing should not proceed without major changes
|
||||
|
||||
### 12.2 Recommendations
|
||||
|
||||
**Recommended Actions Before Processing Begins:**
|
||||
|
||||
1. [Action item 1]
|
||||
2. [Action item 2]
|
||||
3. [Action item 3]
|
||||
|
||||
**Recommended Monitoring/Review Activities:**
|
||||
|
||||
1. [Monitoring item 1]
|
||||
2. [Monitoring item 2]
|
||||
3. [Monitoring item 3]
|
||||
|
||||
### 12.3 Consultation with Supervisory Authority
|
||||
|
||||
**Is consultation with supervisory authority required?**
|
||||
|
||||
☐ No - residual risk is acceptable
|
||||
☐ Yes - high residual risk remains despite mitigation (Article 36)
|
||||
|
||||
**If yes, when will consultation occur?** _______________
|
||||
|
||||
### 12.4 Sign-Off
|
||||
|
||||
**DPIA Completed By:**
|
||||
|
||||
Name: _______________
|
||||
Role: _______________
|
||||
Date: _______________
|
||||
Signature: _______________
|
||||
|
||||
**Reviewed and Approved By:**
|
||||
|
||||
Name: _______________
|
||||
Role: _______________
|
||||
Date: _______________
|
||||
Signature: _______________
|
||||
|
||||
**Next Review Date:** _______________
|
||||
|
||||
*(Recommend annual review or when significant changes occur)*
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Completed Example - Job Candidate Assessment
|
||||
|
||||
This appendix provides a completed example for reference.
|
||||
|
||||
### Example: Job Candidate Assessment Specialist
|
||||
|
||||
**Processing Activity**: AI-powered job candidate assessment tool
|
||||
|
||||
**Personal Data Processed**:
|
||||
- Assessment responses (text)
|
||||
- Communication records (chatbot interactions)
|
||||
- Contact information (name, email) - collected AFTER assessment with consent
|
||||
- Assessment scores/results
|
||||
|
||||
**Purpose**: To assess candidates' suitability for job roles based on their responses to standardized questions
|
||||
|
||||
**Lawful Basis**:
|
||||
- Consent (candidates explicitly consent before providing contact information)
|
||||
- Contract (processing necessary to take steps at request of data subject prior to entering into contract)
|
||||
|
||||
**Automated Decision-Making**: Yes, with human oversight. Candidates are assessed by AI, but:
|
||||
- Contact information only collected AFTER positive assessment
|
||||
- Human recruiter makes final hiring decisions
|
||||
- Candidates can restart assessment at any time
|
||||
- Candidates informed about AI assessment before beginning
|
||||
|
||||
**Key Risks Identified**:
|
||||
1. Bias/discrimination in assessment algorithms - MEDIUM risk
|
||||
2. Lack of transparency about assessment criteria - MEDIUM risk
|
||||
3. Data breach exposing candidate information - LOW risk (after mitigation)
|
||||
|
||||
**Key Mitigation Measures**:
|
||||
- Anonymous assessment until consent obtained
|
||||
- Clear explanation of assessment process
|
||||
- Right to contest results
|
||||
- Human review of all final decisions
|
||||
- Regular bias testing of algorithms
|
||||
- Strong technical security measures (encryption, access controls)
|
||||
- 12-month retention period with secure deletion
|
||||
|
||||
**Residual Risk**: LOW - processing can proceed
|
||||
|
||||
**Special Considerations**:
|
||||
- Candidates must be informed about automated decision-making
|
||||
- Privacy Policy must explain assessment logic
|
||||
- Contact information collected only after explicit consent
|
||||
- Right to human intervention clearly communicated
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Resources and References
|
||||
|
||||
**GDPR Articles Referenced:**
|
||||
- Article 5: Principles relating to processing
|
||||
- Article 6: Lawfulness of processing
|
||||
- Article 9: Special categories of data
|
||||
- Article 13-14: Information to be provided
|
||||
- Article 15-22: Data subject rights
|
||||
- Article 22: Automated decision-making
|
||||
- Article 28: Processor obligations
|
||||
- Article 30: Records of processing activities
|
||||
- Article 33-34: Data breach notification
|
||||
- Article 35: Data Protection Impact Assessment
|
||||
- Article 36: Prior consultation with supervisory authority
|
||||
- Article 45-46: International transfers
|
||||
|
||||
**Additional Guidance:**
|
||||
- WP29 Guidelines on DPIAs (WP 248)
|
||||
- WP29 Guidelines on Automated Decision-Making (WP 251)
|
||||
- ICO DPIA Guidance
|
||||
- EDPB Guidelines on processing personal data for scientific research
|
||||
- Belgian DPA Guidance (https://www.gegevensbeschermingsautoriteit.be)
|
||||
|
||||
**Internal Documents:**
|
||||
- Ask Eve AI Data Protection Agreement
|
||||
- Ask Eve AI Privacy Policy
|
||||
- Technical and Organizational Measures (DPA Annex 2)
|
||||
|
||||
---
|
||||
|
||||
**End of DPIA Template**
|
||||
@@ -4,17 +4,17 @@ No, we do not currently have a formal information security policy document. As a
|
||||
However, we do maintain several key security practices:
|
||||
Product Security (Evie SaaS Platform):
|
||||
|
||||
Multi-tenant architecture with strict data isolation (separate database schemas and object storage folders per tenant)
|
||||
Hosted exclusively with European providers (Scaleway, Bunny.net, Mistral) compliant with EU regulations
|
||||
Published Privacy Policy and Terms & Conditions
|
||||
GDPR-compliant data handling practices
|
||||
- Multi-tenant architecture with strict data isolation (separate database schemas and object storage folders per tenant)
|
||||
- Hosted exclusively with European providers (Scaleway, Bunny.net, Mistral) compliant with EU regulations
|
||||
- Published Privacy Policy and Terms & Conditions
|
||||
- GDPR-compliant data handling practices
|
||||
|
||||
Internal Operations Security:
|
||||
|
||||
Secure password and credential management using Proton Pass
|
||||
All internal business data maintained in secure cloud environments (Proton, Dropbox, Canva)
|
||||
Code versioning and backup through GitHub
|
||||
Controlled access to all systems and services
|
||||
- Secure password and credential management using Proton Pass
|
||||
- All internal business data maintained in secure cloud environments (Proton, Dropbox, Canva)
|
||||
- Code versioning and backup through GitHub
|
||||
- Controlled access to all systems and services
|
||||
|
||||
We plan to formalise our security practices into a comprehensive security policy as our organisation scales beyond 10 employees.
|
||||
|
||||
|
||||
@@ -170,13 +170,7 @@ personal information is gathered:
|
||||
> contact or provide us with information to establish your identity or
|
||||
> age.
|
||||
|
||||
>
|
||||
|
||||
>
|
||||
|
||||
> \
|
||||
|
||||
**Technical Data:**\
|
||||
**Technical Data:**\\
|
||||
When you visit, use, or interact with the Services, we receive the
|
||||
following information about your visit, use, or interactions ("Technical
|
||||
Information"):
|
||||
@@ -253,11 +247,11 @@ and not attempt to reidentify the information, unless required by law.
|
||||
|
||||
As noted above, Ask Eve AI may use content the Customer provides Ask Eve
|
||||
AI to improve the Services, for example to train the models that power
|
||||
Ask Eve AI. Read [**our instructions**(opens in a new
|
||||
window)**](https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance) on
|
||||
how you can opt out of our use of your Content to train our models.\
|
||||
Ask Eve AI. Read [\**our instructions*\*(opens in a new
|
||||
window)\*\*](https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance) on
|
||||
how you can opt out of our use of your Content to train our models.\\
|
||||
|
||||
1. 1. ## Instructions {#instructions-3}
|
||||
1. 1. \#\# Instructions {#instructions-3}
|
||||
|
||||
Data Processor shall only Process Personal Data of Data Controller on
|
||||
behalf of the Data Controller and in accordance with this Data
|
||||
@@ -267,12 +261,12 @@ manner, as is reasonably necessary to provide the Services in accordance
|
||||
with the Agreement. Data Controller shall only give instructions that
|
||||
comply with the Data Protection legislation.
|
||||
|
||||
2. 1. ## Applicable mandatory laws {#applicable-mandatory-laws-3}
|
||||
2. 1. \#\# Applicable mandatory laws {#applicable-mandatory-laws-3}
|
||||
|
||||
Data Processor shall only Process as required by applicable mandatory
|
||||
laws and always in compliance with Data Protection Legislation.\
|
||||
laws and always in compliance with Data Protection Legislation.\\
|
||||
|
||||
3. 1. ## Transfer to a third party {#transfer-to-a-third-party-3}
|
||||
3. 1. \#\# Transfer to a third party {#transfer-to-a-third-party-3}
|
||||
|
||||
Data Processor uses functionality of third party services to realise
|
||||
it's functionality. For the purpose of realising Ask Eve AI's
|
||||
@@ -284,7 +278,7 @@ other third party and/or appoint any third party as a sub-processor of
|
||||
Personal Data unless it is legally required or in case of a notification
|
||||
to the Data Controller by which he gives his consent.
|
||||
|
||||
4. 1. ## Transfer to a Third Country {#transfer-to-a-third-country-3}
|
||||
4. 1. \#\# Transfer to a Third Country {#transfer-to-a-third-country-3}
|
||||
|
||||
Data Processor shall not transfer Personal Data (including any transfer
|
||||
via electronic media) to any Third Country without the prior written
|
||||
@@ -305,9 +299,9 @@ Data Controller about the particular measures taken to guarantee the
|
||||
protection of the Personal Data of the Data Subject in accordance with
|
||||
the Regulation.
|
||||
|
||||
\
|
||||
\\
|
||||
|
||||
5. 1. ## Data secrecy {#data-secrecy-3}
|
||||
5. 1. \#\# Data secrecy {#data-secrecy-3}
|
||||
|
||||
The Data Processor shall maintain data secrecy in accordance with
|
||||
applicable Data Protection Legislation and shall take all reasonable
|
||||
@@ -324,7 +318,7 @@ steps to ensure that:
|
||||
> in accordance with applicable Data Protection Legislation and at all
|
||||
> times act in compliance with the Data Protection Obligations.
|
||||
|
||||
6. 1. ## Appropriate technical and organizational measures {#appropriate-technical-and-organizational-measures-3}
|
||||
6. 1. \#\# Appropriate technical and organizational measures {#appropriate-technical-and-organizational-measures-3}
|
||||
|
||||
Data Processor has implemented (and shall comply with) all appropriate
|
||||
technical and organizational measures to ensure the security of the
|
||||
@@ -348,7 +342,7 @@ registration, de-registration and withdrawal of automation access codes
|
||||
(API Keys), and is also responsible for the complete physical security
|
||||
of its environment.
|
||||
|
||||
7. 1. ## Assistance and co-operation {#assistance-and-co-operation-3}
|
||||
7. 1. \#\# Assistance and co-operation {#assistance-and-co-operation-3}
|
||||
|
||||
The Data Processor shall provide the Data Controller with such
|
||||
assistance and co-operation as the Data Controller may reasonably
|
||||
@@ -358,7 +352,7 @@ Data processed by the Data Processor, including but not limited to:
|
||||
|
||||
> \(1\) on request of the Data Controller, promptly providing written
|
||||
> information regarding the technical and organizational measures which
|
||||
> the Data Processor has implemented to safeguard Personal Data;\
|
||||
> the Data Processor has implemented to safeguard Personal Data;\\
|
||||
|
||||
> \(2\) disclosing full and relevant details in respect of any and all
|
||||
> government, law enforcement or other access protocols or controls
|
||||
@@ -401,7 +395,7 @@ Data processed by the Data Processor, including but not limited to:
|
||||
> Processor shall support the Data Controller in the provision of such
|
||||
> information when explicitly requested by the Data Controller.
|
||||
|
||||
4. # Audit {#audit-1}
|
||||
4. \# Audit {#audit-1}
|
||||
|
||||
At the Data Controller's request the Data Processor shall provide the
|
||||
Data Controller with all information needed to demonstrate that it
|
||||
@@ -423,7 +417,7 @@ minimum, and the Data Controller shall impose sufficient confidentiality
|
||||
obligations on its auditors. Every auditor who does an inspection will
|
||||
be at all times accompanied by a dedicated employee of the Processor.
|
||||
|
||||
4. # Liability {#liability-1}
|
||||
4. \# Liability {#liability-1}
|
||||
|
||||
Each Party shall be liable for any suffered foreseeable, direct and
|
||||
personal damages ("Direct Damages") resulting from any attributable
|
||||
@@ -458,7 +452,7 @@ immediately prior to the cause of damages. In no event shall the Data
|
||||
Processor be held liable if the Data Processor can prove he is not
|
||||
responsible for the event or cause giving rise to the damage.
|
||||
|
||||
4. # Term {#term-1}
|
||||
4. \# Term {#term-1}
|
||||
|
||||
This Data Processing Agreement shall be valid for as long as the
|
||||
Customer uses the Services.
|
||||
@@ -469,7 +463,7 @@ use of Personal Data and delete all Personal Data and copies thereof in
|
||||
its possession unless otherwise agreed or when deletion of the Personal
|
||||
Data should be technically impossible.
|
||||
|
||||
4. # Governing law -- jurisdiction {#governing-law-jurisdiction-1}
|
||||
4. \# Governing law -- jurisdiction {#governing-law-jurisdiction-1}
|
||||
|
||||
This Data Processing Agreement and any non-contractual obligations
|
||||
arising out of or in connection with it shall be governed by and
|
||||
@@ -490,79 +484,6 @@ The Data Controller hereby agrees to the following list of
|
||||
Sub-Processors, engaged by the Data Processor for the Processing of
|
||||
Personal Data under the Agreement:
|
||||
|
||||
+-------------+--------------------------------------------------------+
|
||||
| | |
|
||||
+=============+========================================================+
|
||||
| **Open AI** | |
|
||||
+-------------+--------------------------------------------------------+
|
||||
| Address | OpenAI, L.L.C., |
|
||||
| | |
|
||||
| | 3180 18th St, San Francisco, |
|
||||
| | |
|
||||
| | CA 94110, |
|
||||
| | |
|
||||
| | United States of America. |
|
||||
+-------------+--------------------------------------------------------+
|
||||
| Contact | OpenAI's Data Protection team |
|
||||
| | |
|
||||
| | dsar@openai.com |
|
||||
+-------------+--------------------------------------------------------+
|
||||
| Description | Ask Eve AI accesses Open AI's models through Open AI's |
|
||||
| | API to realise it's functionality. |
|
||||
| | |
|
||||
| | Services are GDPR compliant. |
|
||||
+-------------+--------------------------------------------------------+
|
||||
| | |
|
||||
+-------------+--------------------------------------------------------+
|
||||
|
||||
+---------------+------------------------------------------------------+
|
||||
| | |
|
||||
+===============+======================================================+
|
||||
| **StackHero** | |
|
||||
+---------------+------------------------------------------------------+
|
||||
| Address | Stackhero |
|
||||
| | |
|
||||
| | 1 rue de Stockholm |
|
||||
| | |
|
||||
| | 75008 Paris |
|
||||
| | |
|
||||
| | France |
|
||||
+---------------+------------------------------------------------------+
|
||||
| Contact | support@stackhero.io |
|
||||
+---------------+------------------------------------------------------+
|
||||
| Description | StackHero is Ask Eve AI's cloud provider, and hosts |
|
||||
| | the services for PostgreSQL, Redis, Docker, Minio |
|
||||
| | and Greylog. |
|
||||
| | |
|
||||
| | Services are GDPR compliant. |
|
||||
+---------------+------------------------------------------------------+
|
||||
| **** | |
|
||||
+---------------+------------------------------------------------------+
|
||||
|
||||
+----------------+-----------------------------------------------------+
|
||||
| | |
|
||||
+================+=====================================================+
|
||||
| **A2 Hosting** | |
|
||||
+----------------+-----------------------------------------------------+
|
||||
| Address | A2 Hosting, Inc. |
|
||||
| | |
|
||||
| | PO Box 2998 |
|
||||
| | |
|
||||
| | Ann Arbor, MI 48106 |
|
||||
| | |
|
||||
| | United States |
|
||||
+----------------+-----------------------------------------------------+
|
||||
| Contact | [*+1 734-222-4678*](tel:+1(734)222-4678) |
|
||||
+----------------+-----------------------------------------------------+
|
||||
| Description | A2 hosting is hosting our main webserver and |
|
||||
| | mailserver. They are all hosted on European servers |
|
||||
| | (Iceland). It does not handle data of our business |
|
||||
| | applications. |
|
||||
| | |
|
||||
| | Services are GDPR compliant. |
|
||||
+----------------+-----------------------------------------------------+
|
||||
| **** | |
|
||||
+----------------+-----------------------------------------------------+
|
||||
|
||||
# Annex 2
|
||||
|
||||
@@ -614,7 +535,7 @@ infrastructure. Ask Eve AI uses an intent-based approach where
|
||||
activities are constantly monitored, analysed and benchmarked instead of
|
||||
relying solely on a simple authentication/authorization trust model.
|
||||
|
||||
4. 1. ## General Governance & Awareness {#general-governance-awareness-3}
|
||||
4. 1. \#\# General Governance & Awareness {#general-governance-awareness-3}
|
||||
|
||||
As a product company, Ask Eve AI is committed to maintain and preserve
|
||||
an IT infrastructure that has a robust security architecture, complies
|
||||
@@ -676,7 +597,7 @@ enabled.
|
||||
|
||||
Key management governance is implemented and handled by Facilities.
|
||||
|
||||
1. 1. ## Endpoint Security & User Accounts {#endpoint-security-user-accounts-3}
|
||||
1. 1. \#\# Endpoint Security & User Accounts {#endpoint-security-user-accounts-3}
|
||||
|
||||
All endpoints and any information stored are encrypted using
|
||||
enterprise-grade encryption on all operating systems supported by Ask
|
||||
@@ -701,7 +622,7 @@ ensure endpoint integrity and policy compliance.
|
||||
Access is managed according to role-based access control principles and
|
||||
all user behavior on Ask Eve AI platforms is audited.
|
||||
|
||||
1. 1. ## Data Storage, Recovery & Securing Personal Data {#data-storage-recovery-securing-personal-data-3}
|
||||
1. 1. \#\# Data Storage, Recovery & Securing Personal Data {#data-storage-recovery-securing-personal-data-3}
|
||||
|
||||
> Ask Eve AI has deployed:
|
||||
|
||||
@@ -720,7 +641,7 @@ all user behavior on Ask Eve AI platforms is audited.
|
||||
- Records of the processing activities.
|
||||
- Data Retention Policies
|
||||
|
||||
1. 1. ## Protection & Insurance {#protection-insurance-3}
|
||||
1. 1. \#\# Protection & Insurance {#protection-insurance-3}
|
||||
|
||||
Ask Eve AI has a cyber-crime insurance policy. Details on the policy can
|
||||
be requested through the legal department.
|
||||
1143
content/dpa/1.1/1.1.0.md
Normal file
1143
content/dpa/1.1/1.1.0.md
Normal file
File diff suppressed because it is too large
Load Diff
2364
content/partner agreement/partnership_agreement_v1.md
Normal file
2364
content/partner agreement/partnership_agreement_v1.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ To access certain features of the Service, you must register for an account. You
|
||||
|
||||
### 4. Privacy
|
||||
|
||||
Your use of the Service is also governed by our Privacy Policy, which can be found [here](/content/privacy).
|
||||
Your use of the Service is also governed by our Privacy Policy, which can be found [here](/content/dpa).
|
||||
|
||||
### 5. Intellectual Property
|
||||
|
||||
|
||||
454
content/terms/1.1/1.1.0.md
Normal file
454
content/terms/1.1/1.1.0.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# Terms of Service
|
||||
|
||||
## Ask Eve AI
|
||||
|
||||
**Version 1.0.0**
|
||||
**Effective Date: October 3, 2025**
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
These Terms of Service ("Terms") constitute a legally binding agreement between **Flow IT BV**, with registered office at Toekomststraat 62, 9800 Deinze, Belgium, with company number BE0877.273.542, operating under the trademark **Ask Eve AI** ("Ask Eve AI," "AskEveAI," "we," "us," or "our"), and the Customer (as defined below) that governs the use of the Services (as defined below).
|
||||
|
||||
By signing up to use the Services, accessing the Services, or clicking to accept these Terms, you ("Customer," "you," or "your") agree to be bound by these Terms. You represent that you are lawfully able to enter into contracts and, if you are entering into these Terms on behalf of an entity, that you have legal authority to bind that entity.
|
||||
|
||||
**For commercial customers**: Your use of the Services is also subject to our [Data Protection Agreement](link-to-dpa), which governs the processing of personal data. In the event of any conflict between these Terms and the Data Protection Agreement regarding data protection matters, the Data Protection Agreement shall prevail.
|
||||
|
||||
---
|
||||
|
||||
## 1. Services
|
||||
|
||||
### 1.1 Provision of Services
|
||||
|
||||
1. Upon payment of the applicable fees, Ask Eve AI grants to Customer a non-exclusive, non-transferable, non-sublicensable right to access and use the Ask Eve AI platform ("Platform" or "Services") during the term as stated in these Terms and as specified in the applicable subscription for Customer's business operations.
|
||||
|
||||
2. Ask Eve AI may subcontract to third parties any part of the Services. In particular, Ask Eve AI utilizes third-party service providers to provide, amongst others, connectivity, AI services (including large language models), data centre services, database services, content delivery, and security services. A complete list of Sub-Processors is available in Annex 1 of our Data Protection Agreement.
|
||||
|
||||
3. Customer must provide accurate and up-to-date account information. Customer is responsible for all activities that occur under its account, including the activities of any authorized user or Partner. Customer shall:
|
||||
- Notify Ask Eve AI immediately of any unauthorized use of any password, API key, or user ID, or any other known or suspected breach of security
|
||||
- Use reasonable efforts to stop any unauthorized use of the Services that is known or suspected by Customer
|
||||
- Not provide false identity information to gain access to or use the Services
|
||||
- Maintain proper access controls for all users and API credentials
|
||||
|
||||
### 1.2 Limitations on Use of Services
|
||||
|
||||
1. **Prohibited Actions**: Customer shall not:
|
||||
- Remove any identification, proprietary, copyright, or other notices in the Services or documentation
|
||||
- Represent that output was human-generated when it was not
|
||||
- Reverse engineer the Services into source code, decompile, disassemble, or analyze the Services by "reverse engineering"
|
||||
- Create derivative works of the Services
|
||||
- Merge the Services with other software
|
||||
- Sublicense, sell, lease, or otherwise encumber rights granted by Ask Eve AI (unless expressly authorized by Ask Eve AI in writing)
|
||||
- Use the Services in any way that causes, or may cause, damage to the Services or impairment of the availability or accessibility of the Services
|
||||
- Use the Services in any way that is unlawful, illegal, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity
|
||||
- Attempt to gain unauthorized access to any portion of the Services or related systems or networks
|
||||
- Overload, flood, or perform denial-of-service attacks on the Services
|
||||
- Use automated means to access the Services except through approved APIs and within documented rate limits
|
||||
|
||||
2. **Prohibited Content**: Customer shall not use the Services to create, upload, transmit, distribute, or store content that:
|
||||
- Is illegal, including content depicting or facilitating child exploitation, terrorism, illegal drugs, or other criminal activity
|
||||
- Contains malware, viruses, or malicious code
|
||||
- Infringes intellectual property rights, including pirated material or unauthorized use of trademarks
|
||||
- Constitutes spam, phishing attempts, or fraudulent schemes
|
||||
- Includes personal data without proper consent or legal basis under applicable data protection laws
|
||||
- Promotes hate speech, violence, or discrimination
|
||||
- Attempts to manipulate AI systems to produce harmful, misleading, or unauthorized outputs
|
||||
- Creates deepfakes or other misleading content intended to deceive
|
||||
- Violates any applicable laws or regulations
|
||||
|
||||
3. **Enforcement**: In case of infringement of these limitations, Ask Eve AI reserves all rights to prove and obtain compensation for its full damages incurred by such infringement. This provision does not prevent Ask Eve AI from obtaining equitable relief in summary or other proceedings. Ask Eve AI may immediately suspend or terminate access to the Services upon discovery of any violation.
|
||||
|
||||
### 1.3 Acceptable Use and Compliance
|
||||
|
||||
1. **Data Protection Compliance**:
|
||||
- **Customers** and **Partners** must comply with all applicable data protection laws, including the General Data Protection Regulation (GDPR) and the Belgian Data Protection Act, when using the Services.
|
||||
- Customers and Partners are responsible for obtaining all necessary consents, authorizations, and legal bases required to process personal data through the Services.
|
||||
- Customers and Partners must ensure their end users are properly informed about data processing activities and that appropriate privacy notices are provided.
|
||||
- Although Ask Eve AI provides consent management functionality within the Platform, Customers and Partners remain solely responsible for ensuring their use of the Services complies with all applicable data protection requirements.
|
||||
|
||||
2. **Customer and Partner Indemnification for GDPR Violations**: Customer and Partner agree to indemnify, defend, and hold Ask Eve AI harmless from and against any claims, damages, losses, liabilities, costs, and expenses (including reasonable legal fees) arising from or related to Customer's or Partner's failure to comply with GDPR or other applicable data protection laws.
|
||||
|
||||
3. **Export Controls and Trade Compliance**: Customer certifies that it will comply with all applicable EU trade restrictions, export controls, and economic sanctions. Customer represents and warrants that it will not use the Services in any country or territory subject to EU or international sanctions, or in violation of any applicable trade restrictions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Content
|
||||
|
||||
### 2.1 Input and Output
|
||||
|
||||
1. Customer may provide input to the Services ("Input") and receive output from the Services based on the Input ("Output"). Input and Output are collectively "Content."
|
||||
|
||||
2. Customer is responsible for all Content, including ensuring that it does not violate any applicable law or these Terms. Customer represents and warrants that it has all rights, licenses, and permissions needed to provide Input to the Services.
|
||||
|
||||
### 2.2 Ownership
|
||||
|
||||
1. **Customer Ownership**: Customer:
|
||||
- Retains all ownership rights in Input
|
||||
- Owns all Output generated by the Services based on Customer's Input
|
||||
- Owns all specialist configurations, prompts, business logic, and custom implementations created by Customer on the Platform
|
||||
|
||||
2. **Ask Eve AI Assignment**: Ask Eve AI hereby assigns to Customer all of our right, title, and interest, if any, in and to Output generated specifically for Customer.
|
||||
|
||||
3. **Platform Ownership**: Ask Eve AI retains all ownership rights in and to the Platform itself, including all software, improvements, enhancements, modifications, AI models, core functionality, and intellectual property rights related thereto.
|
||||
|
||||
### 2.3 Non-Unique Outputs
|
||||
|
||||
Due to the nature of AI services and machine learning generally, Output may not be unique. Other users may receive similar output from the Services. Ask Eve AI's assignment of Output to Customer does not extend to other users' output or any third-party output.
|
||||
|
||||
### 2.4 Use of Content by Ask Eve AI
|
||||
|
||||
Ask Eve AI may use Content to:
|
||||
- Provide, maintain, develop, and improve the Services
|
||||
- Comply with applicable law
|
||||
- Enforce our terms and policies
|
||||
- Keep the Services safe and secure
|
||||
- Generate aggregated or de-identified data for research, development, and model improvement, subject to the opt-out provisions in our Data Protection Agreement
|
||||
|
||||
### 2.5 Nature of AI and Customer Responsibilities
|
||||
|
||||
1. **AI Limitations**: Artificial intelligence and machine learning are rapidly evolving fields. Ask Eve AI is constantly working to improve the Services to make them more accurate, reliable, safe, and beneficial. However, given the probabilistic nature of machine learning, use of the Services may, in some situations, result in Output that does not accurately reflect real people, places, or facts.
|
||||
|
||||
2. **Customer Acknowledgments**: When Customer uses the Services, Customer understands and agrees that:
|
||||
- **Output may not always be accurate**: Customer should not rely on Output from the Services as a sole source of truth or factual information, or as a substitute for professional advice
|
||||
- **Human review required**: Customer must evaluate Output for accuracy and appropriateness for its use case, including using human review as appropriate, before using or sharing Output from the Services
|
||||
- **No automated decisions affecting individuals**: Customer must not use any Output relating to a person for any purpose that could have a legal or material impact on that person, such as making credit, educational, employment, housing, insurance, legal, medical, or other important decisions about them, without appropriate human oversight and intervention
|
||||
- **Potential for inappropriate content**: The Services may provide incomplete, incorrect, or offensive Output that does not represent Ask Eve AI's views
|
||||
- **No endorsements**: If Output references any third-party products or services, it does not mean the third party endorses or is affiliated with Ask Eve AI
|
||||
|
||||
---
|
||||
|
||||
## 3. Intellectual Property
|
||||
|
||||
### 3.1 Ask Eve AI Ownership
|
||||
|
||||
Except as expressly set forth in these Terms, Ask Eve AI owns and retains all right, title, and interest in and to the Services, including:
|
||||
- The Platform with all software, improvements, enhancements, or modifications thereto
|
||||
- Any software, applications, inventions, or other technology developed as part of any maintenance or support
|
||||
- All AI models, algorithms, and training methodologies
|
||||
- All Intellectual Property Rights related to any of the foregoing
|
||||
|
||||
"Intellectual Property Rights" means current and future worldwide rights under patent, copyright, trade secret, trademark, moral rights, and other similar rights.
|
||||
|
||||
### 3.2 Reservation of Rights
|
||||
|
||||
All rights in and to Ask Eve AI not expressly granted to Customer in these Terms are reserved by Ask Eve AI. No license is granted to Customer except as to use of the Services as expressly stated herein. These Terms do not grant Customer:
|
||||
- Any rights to the Intellectual Property Rights in the Platform or Services
|
||||
- Any rights to use the Ask Eve AI trademarks, logos, domain names, or other brand features unless otherwise agreed in writing
|
||||
|
||||
### 3.3 Partner Implementations
|
||||
|
||||
Where Partners implement functionality on the Platform involving Ask Eve AI:
|
||||
- Partners retain ownership of their specific implementations, configurations, and custom code
|
||||
- Partners grant Ask Eve AI a license to host, operate, and provide their implementations as part of the Services
|
||||
- Ask Eve AI retains ownership of the underlying Platform infrastructure and core functionality
|
||||
- Partners are responsible for ensuring their implementations comply with these Terms and all applicable laws
|
||||
|
||||
---
|
||||
|
||||
## 4. Pricing and Payment
|
||||
|
||||
### 4.1 Subscription Model
|
||||
|
||||
1. **Paid Subscriptions**: Customer can only purchase a paid subscription ("Paid Subscription") by paying Basic Fees in advance on a monthly or yearly basis, or at another recurring interval agreed upon prior to purchase, through a third-party payment platform as indicated by Ask Eve AI.
|
||||
|
||||
2. **Third-Party Payment Terms**: Where payment is processed through a third-party payment platform, the separate terms and conditions of that payment platform shall apply in addition to these Terms.
|
||||
|
||||
### 4.2 Fee Structure
|
||||
|
||||
1. **Basic Fees**: Prepaid fees for the base subscription tier, covering specified usage limits for the billing period. Basic Fees must be paid in advance for each billing period to maintain access to the Services.
|
||||
|
||||
2. **Additional Fees**: Additional Fees will be charged to Customer on a monthly basis on top of the Basic Fees when the effective usage of the Services exceeds the usage limits covered by the Basic Fee for the respective month. Additional Fees will be calculated and invoiced to Customer through the same third-party payment platform.
|
||||
|
||||
3. **Overage Options**:
|
||||
- Customer may enable or disable overage usage for each service element (storage, embeddings, interactions) as defined in the subscription agreement
|
||||
- If overage is disabled and usage limits are reached, Services will be suspended until the next billing period or until Customer enables overage
|
||||
- Customer may request changes to overage settings mid-period by contacting Ask Eve AI or their managing Partner
|
||||
- Usage metrics are displayed in the administrative interface
|
||||
|
||||
### 4.3 Payment Terms
|
||||
|
||||
1. **Currency and Taxes**: All prices are quoted in EUR unless otherwise agreed. Tax rates are calculated based on the information Customer provides and the applicable rate at the time of payment. Prices do not include VAT, which will be added at the applicable rate.
|
||||
|
||||
2. **Billing Cycle**: Unless otherwise specified between the Parties, Paid Subscriptions will continue indefinitely until cancelled. Customer will receive a recurring invoice on the first day of each billing period for Basic Fees and will authorize the applicable third-party payment platform to charge the payment method for the then-current subscription fee.
|
||||
|
||||
3. **Payment Deadline**: Payment of each invoiced amount for Additional Fees, taxes included, must be completed within thirty (30) days after the date of the invoice.
|
||||
|
||||
4. **Late Payment**: Any payment after the fixed payment date shall be subject to delay interest for late payment in accordance with the Law of 2 August 2002 on combating late payment in commercial transactions, calculated at the legal interest rate as determined by the Belgian government. This provision shall not in any event exclude the possible payment of damages.
|
||||
|
||||
5. **Invoice Complaints**: Complaints relating to invoices must be notified to Ask Eve AI directly and in writing within fifteen (15) days after the invoice date via registered letter or via a proven received email to finance@askeveai.com, stating the precise nature and extent of the complaints.
|
||||
|
||||
### 4.4 Cancellation and Refunds
|
||||
|
||||
1. **Customer Cancellation**: Customer may cancel a Paid Subscription at any time by following the cancellation instructions provided in the administrative interface or by contacting Ask Eve AI. Unless otherwise stated, cancellation will take effect at the end of the billing period in which Customer cancels.
|
||||
|
||||
2. **No Refunds**: Ask Eve AI does not offer refunds or reimbursements for partial subscription periods unless otherwise agreed between the Parties in writing.
|
||||
|
||||
3. **Ask Eve AI Termination**: In addition to, and without prejudice to any other rights Ask Eve AI may have under these Terms, Ask Eve AI reserves the right to terminate a Paid Subscription at any time upon at least fourteen (14) days' notice. Unless Ask Eve AI notifies Customer otherwise, Ask Eve AI will grant Customer access to the Paid Subscription for the remainder of the then-current billing period.
|
||||
|
||||
### 4.5 Price Changes
|
||||
|
||||
Ask Eve AI may from time to time change the prices for Paid Subscriptions, including recurring Basic Fees and Additional Fees, in response to circumstances such as:
|
||||
- Changes to product offerings and features
|
||||
- Changes in business operations or economic environment
|
||||
- Changes in costs from subcontractors or service providers
|
||||
- Security, legal, or regulatory reasons
|
||||
|
||||
Ask Eve AI will provide reasonable notice of price changes by any reasonable means, including by email or in-app notice, which will in any event not be less than fourteen (14) days. Price changes will become effective at the start of the next subscription period following the date of the price change.
|
||||
|
||||
Subject to applicable law, Customer will have accepted the new price by continuing to use the Services after the new price comes into effect. If Customer does not agree to a price change, Customer may reject the change by unsubscribing from the applicable Paid Subscription before the price change comes into effect.
|
||||
|
||||
---
|
||||
|
||||
## 5. Suspension and Termination
|
||||
|
||||
### 5.1 Suspension for Non-Payment
|
||||
|
||||
1. **Basic Fees**: If Basic Fees are not paid when due, Ask Eve AI reserves the right to immediately suspend Customer's access to the Services without prior notice.
|
||||
|
||||
2. **Additional Fees**: If Additional Fees are not paid within thirty (30) days of the invoice date, Ask Eve AI may suspend Customer's access to the Services.
|
||||
|
||||
3. **Reactivation**: Suspended accounts may be reactivated upon payment of all outstanding amounts. However, time elapsed during suspension still counts toward the applicable billing period, and no pro-rata refunds or credits will be provided.
|
||||
|
||||
### 5.2 Immediate Termination by Ask Eve AI
|
||||
|
||||
Ask Eve AI reserves the right to suspend or terminate Customer's access to the Services or delete Customer's account immediately without any notice, compensation, or court intervention if Ask Eve AI determines:
|
||||
|
||||
1. Customer has breached these Terms, including violation of Section 1.2 (Limitations on Use of Services) or Section 1.3 (Acceptable Use and Compliance)
|
||||
2. Customer becomes insolvent, files a petition of bankruptcy (or any similar petition under any insolvency law of any jurisdiction), ceases its activities, or proposes any dissolution
|
||||
3. Ask Eve AI must do so to comply with applicable law
|
||||
4. Customer's use of the Services could cause risk or harm to Ask Eve AI, its users, or anyone else
|
||||
|
||||
### 5.3 Service Discontinuation
|
||||
|
||||
Ask Eve AI may decide to discontinue the Services. In such case, Ask Eve AI will give Customer advance notice and a refund for any prepaid, unused Services on a pro-rata basis.
|
||||
|
||||
### 5.4 Data Upon Termination
|
||||
|
||||
1. **License Suspension**: When a subscription is suspended or cancelled, Customer loses access to the Services, but tenant data is not automatically deleted. Customer may resume access by reactivating the subscription and paying applicable fees.
|
||||
|
||||
2. **Tenant Termination**: Customer may request full termination of its tenant account and deletion of all associated tenant data by contacting Ask Eve AI. Upon such request:
|
||||
- Tenant-specific content will be isolated and marked for deletion
|
||||
- Deletion will occur within ninety (90) days as specified in the Data Protection Agreement
|
||||
- Financial and billing records will be retained for seven (7) years as required by Belgian law
|
||||
- User accounts will be disabled to maintain audit trail integrity
|
||||
|
||||
3. **Data Export**: Customer may export accessible data through the API while subscription remains active and fees are current. Ask Eve AI does not provide separate data export services.
|
||||
|
||||
---
|
||||
|
||||
## 6. Warranties and Disclaimers
|
||||
|
||||
### 6.1 Service Availability
|
||||
|
||||
Ask Eve AI strives to provide high availability of the Services but does not guarantee any specific uptime or service level. Ask Eve AI reserves the right to:
|
||||
- Perform scheduled maintenance between 22:00 and 05:00 CET without prior notice
|
||||
- Perform scheduled maintenance outside these hours with at least seven (7) days' advance notice
|
||||
- Perform emergency maintenance at any time without notice when necessary to protect the security, integrity, or availability of the Services
|
||||
|
||||
### 6.2 Warranty Disclaimer
|
||||
|
||||
THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE." TO THE FULLEST EXTENT PERMITTED BY LAW, ASK EVE AI AND ITS PARTNERS MAKE NO WARRANTY OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
|
||||
|
||||
Specifically, Ask Eve AI does not warrant that:
|
||||
- The Services will meet Customer's performance requirements or operate in accordance with Customer's expectations
|
||||
- The Services will be uninterrupted, secure, or error-free
|
||||
- Any errors or defects will be corrected
|
||||
- The Services will be free from viruses or other harmful components
|
||||
- Results obtained from use of the Services will be accurate or reliable
|
||||
|
||||
Customer acknowledges that before entering into these Terms, Customer has evaluated the Services and accepts responsibility for selection of the Services, their use, and the results to be obtained therefrom.
|
||||
|
||||
### 6.3 AI-Specific Disclaimers
|
||||
|
||||
Neither Ask Eve AI nor its partners make any warranty about:
|
||||
- The accuracy, completeness, or appropriateness of any Output generated by the Services
|
||||
- Any content or information in or from an end user or Customer account
|
||||
- The reliability of AI models or the absence of AI hallucinations, errors, or biases
|
||||
- The suitability of Output for any particular purpose or decision-making process
|
||||
|
||||
Customer accepts and agrees that any use of Output from the Services is at Customer's sole risk and that Customer will not rely on Output as a sole source of truth or factual information, or as a substitute for professional advice.
|
||||
|
||||
---
|
||||
|
||||
## 7. Limitation of Liability
|
||||
|
||||
### 7.1 Liability Cap
|
||||
|
||||
TO THE FULLEST EXTENT PERMITTED BY LAW, THE TOTAL AGGREGATE LIABILITY OF ASK EVE AI UNDER THESE TERMS SHALL BE LIMITED TO THE TOTAL AMOUNT OF BASIC FEES PAID BY CUSTOMER TO ASK EVE AI DURING THE THREE (3) MONTHS IMMEDIATELY PRIOR TO THE EVENT GIVING RISE TO THE LIABILITY. ADDITIONAL FEES (OVERAGE) ARE EXCLUDED FROM THIS CALCULATION.
|
||||
|
||||
### 7.2 Exclusion of Consequential Damages
|
||||
|
||||
IN NO EVENT SHALL ASK EVE AI BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO:
|
||||
- Loss of profits or revenue
|
||||
- Loss of business or anticipated savings
|
||||
- Loss of goodwill or reputation
|
||||
- Loss of data or information
|
||||
- Business interruption
|
||||
- Cost of procurement of substitute services
|
||||
- Any other indirect or consequential loss or damage
|
||||
|
||||
This exclusion applies regardless of the legal theory on which the claim is based (contract, tort, negligence, strict liability, or otherwise) and whether or not Ask Eve AI has been advised of the possibility of such damages.
|
||||
|
||||
### 7.3 Specific Exclusions
|
||||
|
||||
Ask Eve AI shall have no liability whatsoever for:
|
||||
- **AI Output**: Any damages or claims resulting from Customer's use of, reliance on, or decisions made based on Output generated by the Services
|
||||
- **Third-Party Services**: Deficiencies in infrastructure services or third-party software provided by Ask Eve AI's Sub-Processors, beyond the liability such Sub-Processors have toward Ask Eve AI
|
||||
- **Customer Content**: Any claims arising from Customer's Input, including claims of infringement, defamation, or violation of privacy rights
|
||||
- **End User Claims**: Claims brought by Customer's end users arising from Customer's use of the Services
|
||||
- **Unauthorized Use**: Damages resulting from unauthorized access to or use of Customer's account
|
||||
- **Force Majeure**: Events beyond Ask Eve AI's reasonable control, including acts of God, natural disasters, war, terrorism, riots, labor disputes, governmental actions, internet disturbances, epidemics, pandemics, or failures of third-party infrastructure providers
|
||||
|
||||
### 7.4 Customer Indemnification
|
||||
|
||||
Customer shall, at its own expense, indemnify, defend, and hold Ask Eve AI harmless from and against any claim(s), damages, losses, liabilities, costs, and expenses (including reasonable legal fees) brought against Ask Eve AI by a third party arising out of or related to:
|
||||
- Customer's use of Output obtained from the Services
|
||||
- Customer's breach of these Terms
|
||||
- Customer's violation of any applicable laws or regulations
|
||||
- Customer's violation of any third-party rights
|
||||
- Customer's failure to comply with GDPR or other data protection laws
|
||||
|
||||
### 7.5 Mandatory Liability
|
||||
|
||||
Nothing in these Terms shall limit or exclude liability to the extent such limitation or exclusion is prohibited by mandatory applicable law, including liability for:
|
||||
- Death or personal injury caused by negligence
|
||||
- Fraud or fraudulent misrepresentation
|
||||
- Intentional misconduct or gross negligence
|
||||
- Any other liability that cannot be excluded or limited under Belgian or EU law
|
||||
|
||||
### 7.6 Basis of the Bargain
|
||||
|
||||
Customer acknowledges and agrees that the limitations of liability set forth in this Section 7 are fundamental elements of the basis of the bargain between Ask Eve AI and Customer, and that Ask Eve AI would not be able to provide the Services on an economically reasonable basis without these limitations.
|
||||
|
||||
---
|
||||
|
||||
## 8. Confidential Information
|
||||
|
||||
### 8.1 Mutual Confidentiality Obligations
|
||||
|
||||
1. **Ask Eve AI's Confidential Information**: Customer acknowledges that information and data (including general business information) it receives from Ask Eve AI concerning the Services and any documentation related to the Services are confidential and proprietary and a valuable commercial asset of Ask Eve AI.
|
||||
|
||||
2. **Customer's Confidential Information**: Ask Eve AI acknowledges that general business information and Customer data it receives from Customer is confidential and proprietary.
|
||||
|
||||
3. **Confidentiality Obligations**: Both Parties agree to:
|
||||
- Keep confidential information received from the other Party in confidence
|
||||
- Not disclose any such information to third parties without prior written consent of the disclosing Party
|
||||
- Not use confidential information for its own benefit or purposes other than fulfilling contractual obligations
|
||||
- Disclose confidential information only to employees or advisors who require the information to enable that Party to fulfill its contractual obligations and who are bound by similar confidentiality obligations
|
||||
|
||||
### 8.2 Exclusions from Confidentiality
|
||||
|
||||
A Party's Confidential Information shall not be deemed to include information that:
|
||||
- Is or becomes publicly known other than through any act or omission of the receiving Party
|
||||
- Was in the receiving Party's lawful possession before the disclosure
|
||||
- Is lawfully disclosed to the receiving Party by a third party without restriction on disclosure
|
||||
- Is independently developed by the receiving Party, which independent development can be shown by written evidence
|
||||
- Is required to be disclosed by law, by any court of competent jurisdiction, or by any regulatory or administrative body
|
||||
|
||||
---
|
||||
|
||||
## 9. Data Protection
|
||||
|
||||
### 9.1 Data Protection Agreement
|
||||
|
||||
For commercial customers, the processing of personal data is governed by our Data Protection Agreement, which is incorporated into these Terms by reference. The Data Protection Agreement can be found at [link to DPA].
|
||||
|
||||
### 9.2 Precedence
|
||||
|
||||
In the event of any conflict between these Terms and the Data Protection Agreement regarding data protection matters, the Data Protection Agreement shall prevail.
|
||||
|
||||
### 9.3 Customer Responsibilities
|
||||
|
||||
Customer is responsible for:
|
||||
- Ensuring it has a lawful basis for processing personal data through the Services
|
||||
- Providing appropriate privacy notices to data subjects
|
||||
- Obtaining necessary consents where required
|
||||
- Responding to data subject rights requests
|
||||
- Implementing appropriate technical and organizational measures for data it controls
|
||||
|
||||
---
|
||||
|
||||
## 10. General Provisions
|
||||
|
||||
### 10.1 Assignment
|
||||
|
||||
Customer may not assign any part of these Terms without Ask Eve AI's prior written consent, except that no such consent will be required with respect to an assignment of these Terms to an Affiliate or in connection with a merger, acquisition, corporate reorganization, or sale of all or substantially all of its assets. Any other attempt to transfer or assign is void.
|
||||
|
||||
Ask Eve AI may assign these Terms or any rights hereunder without Customer's consent.
|
||||
|
||||
### 10.2 Dispute Resolution
|
||||
|
||||
1. **Informal Negotiation**: Before initiating any formal legal proceedings, the Parties agree to first attempt to resolve any dispute, claim, or controversy arising out of or relating to these Terms through good faith negotiations for a period of thirty (30) days.
|
||||
|
||||
2. **Formal Proceedings**: If the dispute cannot be resolved through informal negotiation, either Party may pursue formal legal proceedings, including through a Belgian bailiff (deurwaarder/huissier de justice) or other legal collection methods available under Belgian law.
|
||||
|
||||
### 10.3 Governing Law and Jurisdiction
|
||||
|
||||
These Terms are exclusively governed by Belgian law, without regard to its conflict of laws principles. Any litigation relating to the conclusion, validity, interpretation, and/or performance of these Terms, or any other dispute concerning or related to these Terms, shall be submitted to the exclusive jurisdiction of the courts of Ghent (Gent), Belgium.
|
||||
|
||||
### 10.4 Severability
|
||||
|
||||
If any provision of these Terms is held to be void, invalid, or unenforceable under applicable law, this shall not cause the other provisions of these Terms to be void or unenforceable. In such cases, the Parties shall replace the affected provision with a different provision that is not void or unenforceable and that represents the same intention that the Parties had with the original provision.
|
||||
|
||||
### 10.5 Force Majeure
|
||||
|
||||
Neither Ask Eve AI nor Customer will be liable for inadequate performance to the extent caused by a condition that was beyond the Party's reasonable control, including but not limited to natural disaster, act of war or terrorism, riot, labor condition, governmental action, internet disturbance, epidemic, pandemic, or failure of third-party infrastructure providers.
|
||||
|
||||
Any delay resulting from such causes shall extend performance accordingly or excuse performance, in whole or in part, as may be reasonable under the circumstances. In such an event, each Party shall notify the other Party of the expected duration of the force majeure event.
|
||||
|
||||
### 10.6 Modification of Terms
|
||||
|
||||
1. **Notice of Changes**: Ask Eve AI reserves the right to modify these Terms at any time. We will provide reasonable notice of any material changes to these Terms by any reasonable means, including by email, in-app notification, or by posting notice of the changes on our website, which notice will in any event be provided at least fourteen (14) days before the changes take effect.
|
||||
|
||||
2. **Acceptance**: Customer's continued use of the Services after such modifications will constitute acceptance of the modified Terms. If Customer does not agree to the modified Terms, Customer must discontinue use of the Services and may cancel the subscription in accordance with Section 4.4.
|
||||
|
||||
3. **Non-Material Changes**: Ask Eve AI may make non-material changes (such as corrections of typos, clarifications, or updates to contact information) without advance notice.
|
||||
|
||||
### 10.7 Entire Agreement
|
||||
|
||||
These Terms, together with the Data Protection Agreement and any other documents expressly incorporated by reference, constitute the entire agreement between the Parties concerning the subject matter hereof and supersede all prior agreements, understandings, and arrangements, whether written or oral, relating to such subject matter.
|
||||
|
||||
### 10.8 No Waiver
|
||||
|
||||
The failure of either Party to enforce any provision of these Terms shall not constitute a waiver of that provision or any other provision. No waiver shall be effective unless made in writing and signed by an authorized representative of the waiving Party.
|
||||
|
||||
### 10.9 Notices
|
||||
|
||||
All notices required or permitted under these Terms shall be in writing and shall be deemed given:
|
||||
- When delivered personally
|
||||
- When sent by confirmed email to the email address provided by the receiving Party
|
||||
- Three (3) business days after being sent by registered mail to the address provided by the receiving Party
|
||||
|
||||
Notices to Ask Eve AI should be sent to: legal@askeveai.com
|
||||
|
||||
### 10.10 Language
|
||||
|
||||
These Terms are executed in English. In case of any discrepancy between language versions, the English version shall prevail.
|
||||
|
||||
### 10.11 Survival
|
||||
|
||||
The following provisions shall survive termination or expiration of these Terms: Sections 2.2 (Ownership), 3 (Intellectual Property), 6.2 and 6.3 (Disclaimers), 7 (Limitation of Liability), 8 (Confidential Information), and 10 (General Provisions).
|
||||
|
||||
---
|
||||
|
||||
## Contact Information
|
||||
|
||||
For questions about these Terms, please contact:
|
||||
|
||||
**Ask Eve AI (Flow IT BV)**
|
||||
Toekomststraat 62
|
||||
9800 Deinze
|
||||
Belgium
|
||||
Company Number: BE0877.273.542
|
||||
|
||||
Email: legal@askeveai.com
|
||||
Website: https://askeveai.com
|
||||
|
||||
---
|
||||
|
||||
**By using the Services, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.**
|
||||
|
||||
---
|
||||
|
||||
*Last updated: October 3, 2025*
|
||||
@@ -86,6 +86,7 @@ services:
|
||||
- ../scripts:/app/scripts
|
||||
- ../patched_packages:/app/patched_packages
|
||||
- ./eveai_logs:/app/logs
|
||||
- ../db_backups:/app/db_backups
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -389,22 +390,6 @@ services:
|
||||
networks:
|
||||
- eveai-dev-network
|
||||
|
||||
# flower:
|
||||
# image: ${REGISTRY_PREFIX:-}josakola/flower:latest
|
||||
# build:
|
||||
# context: ..
|
||||
# dockerfile: ./docker/flower/Dockerfile
|
||||
# environment:
|
||||
# <<: *common-variables
|
||||
# volumes:
|
||||
# - ../scripts:/app/scripts
|
||||
# ports:
|
||||
# - "3007:5555" # Dev Flower volgens port schema
|
||||
# depends_on:
|
||||
# - redis
|
||||
# networks:
|
||||
# - eveai-dev-network
|
||||
|
||||
flower:
|
||||
image: mher/flower:latest
|
||||
environment:
|
||||
@@ -468,25 +453,25 @@ services:
|
||||
networks:
|
||||
- eveai-dev-network
|
||||
|
||||
grafana:
|
||||
image: ${REGISTRY_PREFIX:-}josakola/grafana:latest
|
||||
build:
|
||||
context: ./grafana
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3012:3000" # Dev Grafana volgens port schema
|
||||
volumes:
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||
- ./grafana/data:/var/lib/grafana
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- prometheus
|
||||
networks:
|
||||
- eveai-dev-network
|
||||
# grafana:
|
||||
# image: ${REGISTRY_PREFIX:-}josakola/grafana:latest
|
||||
# build:
|
||||
# context: ./grafana
|
||||
# dockerfile: Dockerfile
|
||||
# ports:
|
||||
# - "3012:3000" # Dev Grafana volgens port schema
|
||||
# volumes:
|
||||
# - ./grafana/provisioning:/etc/grafana/provisioning
|
||||
# - ./grafana/data:/var/lib/grafana
|
||||
# environment:
|
||||
# - GF_SECURITY_ADMIN_USER=admin
|
||||
# - GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
# - GF_USERS_ALLOW_SIGN_UP=false
|
||||
# restart: unless-stopped
|
||||
# depends_on:
|
||||
# - prometheus
|
||||
# networks:
|
||||
# - eveai-dev-network
|
||||
|
||||
networks:
|
||||
eveai-dev-network:
|
||||
|
||||
@@ -2,3 +2,5 @@ FROM registry.ask-eve-ai-local.com/josakola/eveai-base:latest
|
||||
# Copy the source code into the container.
|
||||
COPY eveai_ops /app/eveai_ops
|
||||
COPY migrations /app/migrations
|
||||
COPY db_backups /app/db_backups
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from common.models.user import User, Role, Tenant, TenantDomain
|
||||
import common.models.interaction
|
||||
import common.models.entitlements
|
||||
import common.models.document
|
||||
from common.utils.security_utils import enforce_tenant_consent_ui
|
||||
from config.logging_config import configure_logging
|
||||
from common.utils.security import set_tenant_session_data
|
||||
from common.utils.errors import register_error_handlers
|
||||
@@ -109,6 +110,12 @@ def create_app(config_file=None):
|
||||
sqlalchemy_logger.setLevel(logging.DEBUG)
|
||||
# log_request_middleware(app) # Add this when debugging nginx or another proxy
|
||||
|
||||
# Register global consent guard via extension
|
||||
@app.before_request
|
||||
def enforce_tenant_consent():
|
||||
app.logger.debug("Enforcing tenant consent")
|
||||
return enforce_tenant_consent_ui()
|
||||
|
||||
# @app.before_request
|
||||
# def before_request():
|
||||
# # app.logger.debug(f"Before request - Session ID: {session.sid}")
|
||||
|
||||
30
eveai_app/static/assets/css/eveai-consent-viewer.css
Normal file
30
eveai_app/static/assets/css/eveai-consent-viewer.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/* Consent Viewer specific styles */
|
||||
|
||||
/* Ensure markdown text aligns left by default */
|
||||
#consent-document-viewer .markdown-body,
|
||||
.markdown-body {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Make the viewer pane scrollable with a responsive max height */
|
||||
#consent-document-viewer {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Optional readability improvements */
|
||||
#consent-document-viewer .markdown-body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
#consent-document-viewer .markdown-body pre,
|
||||
#consent-document-viewer .markdown-body code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Optional: keep the viewer header visible while scrolling */
|
||||
#consent-viewer-section .card-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: #fff;
|
||||
}
|
||||
53
eveai_app/static/assets/js/eveai-consent-viewer.js
Normal file
53
eveai_app/static/assets/js/eveai-consent-viewer.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Centralized consent viewer JS
|
||||
// Attaches a single delegated click handler to load consent markdown fragments
|
||||
(function() {
|
||||
function initConsentViewer() {
|
||||
const viewerSection = document.getElementById('consent-viewer-section');
|
||||
const viewer = document.getElementById('consent-document-viewer');
|
||||
const vType = document.getElementById('viewer-type');
|
||||
const vVer = document.getElementById('viewer-version');
|
||||
const loading = document.getElementById('viewer-loading');
|
||||
|
||||
if (!viewerSection || !viewer) {
|
||||
// Page without consent viewer; do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', async function(e) {
|
||||
const btn = e.target.closest('.btn-view-consent');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
|
||||
const url = btn.getAttribute('data-url');
|
||||
const type = btn.getAttribute('data-consent-type') || '';
|
||||
const version = btn.getAttribute('data-version') || '';
|
||||
if (!url) {
|
||||
console.warn('Consent viewer: data-url missing on button');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
viewerSection.style.display = 'block';
|
||||
if (vType) vType.textContent = type;
|
||||
if (vVer) vVer.textContent = version;
|
||||
if (loading) loading.style.display = 'block';
|
||||
viewer.innerHTML = '';
|
||||
|
||||
const resp = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const html = await resp.text();
|
||||
viewer.innerHTML = html;
|
||||
} catch (e) {
|
||||
viewer.innerHTML = '<div class="alert alert-danger">Failed to load document.</div>';
|
||||
} finally {
|
||||
if (loading) loading.style.display = 'none';
|
||||
viewerSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initConsentViewer);
|
||||
} else {
|
||||
initConsentViewer();
|
||||
}
|
||||
})();
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card" id="content-viewer-section">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="showRaw">Show Raw</button>
|
||||
<button class="btn btn-sm btn-outline-primary active" id="showRendered">Show Rendered</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body" id="content-document-viewer">
|
||||
<!-- Raw markdown view (hidden by default) -->
|
||||
<div id="rawMarkdown" class="code-wrapper" style="display: none;">
|
||||
<pre><code class="language-markdown">{{ markdown_content }}</code></pre>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
<!-- Gebundelde CSS (bevat nu al je CSS) -->
|
||||
<link href="{{ asset_url('dist/main.css') }}" rel="stylesheet" />
|
||||
<!-- Content viewer specific styles -->
|
||||
<link href="{{ url_for('static', filename='assets/css/eveai-content-viewer.css') }}" rel="stylesheet" />
|
||||
<base href="/admin/">
|
||||
</head>
|
||||
|
||||
|
||||
@@ -70,8 +70,14 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ dropdown('Tenants', 'source_environment', [
|
||||
{'name': 'Tenants', 'url': 'user/tenants', 'roles': ['Super User', 'Partner Admin']},
|
||||
{'name': 'Consent Versions', 'url': 'user/consent_versions', 'roles': ['Super User']},
|
||||
{'name': 'Tenant Overview', 'url': 'user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Edit Tenant', 'url': 'user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Consent History', 'url': 'user/tenant_consents_history', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Consent Status', 'url': 'user/consent/tenant', 'roles': ['Tenant Admin']},
|
||||
{'name': 'Tenant Consent Status', 'url': 'user/tenants/' ~ session['tenant'].get('id') ~ '/consents', 'roles': ['Super User', 'Partner Admin']},
|
||||
{'name': 'Consent Renewal', 'url': 'user/consent/tenant_renewal', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
|
||||
{'name': 'Tenant Partner Services', 'url': 'user/tenant_partner_services', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Makes', 'url': 'user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
{'name': 'Tenant Projects', 'url': 'user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||
|
||||
13
eveai_app/templates/user/consent_renewal.html
Normal file
13
eveai_app/templates/user/consent_renewal.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Consent Renewal{% endblock %}
|
||||
{% block content_title %}Consent Renewal{% endblock %}
|
||||
{% block content_description %}Consent renewal process{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="alert alert-info">This page will guide you through the consent renewal process.</div>
|
||||
<p>Please navigate to the Tenant Consents page to renew the required consents.</p>
|
||||
<a href="{{ prefixed_url_for('user_bp.tenant_consent') }}" class="btn btn-primary">Go to Tenant Consents</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
75
eveai_app/templates/user/consent_version.html
Normal file
75
eveai_app/templates/user/consent_version.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field, render_included_field %}
|
||||
|
||||
{% block title %}Create or Edit Consent Version{% endblock %}
|
||||
|
||||
{% block content_title %}Create or Edit Consent Version{% endblock %}
|
||||
{% block content_description %}Create or Edit Consent Version{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Consent Version</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// JavaScript om de gebruiker's timezone te detecteren
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Detect timezone
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Send timezone to the server via a POST request
|
||||
fetch('set_user_timezone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ timezone: userTimezone })
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
console.log('Timezone sent to server successfully');
|
||||
} else {
|
||||
console.error('Failed to send timezone to server');
|
||||
}
|
||||
});
|
||||
|
||||
$('#timezone').select2({
|
||||
placeholder: 'Selecteer een timezone...',
|
||||
allowClear: true,
|
||||
theme: 'bootstrap',
|
||||
width: '100%',
|
||||
dropdownAutoWidth: true,
|
||||
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
|
||||
scrollAfterSelect: false,
|
||||
// Verbeterd scroll gedrag
|
||||
dropdownParent: $('body')
|
||||
});
|
||||
|
||||
// Stel de huidige waarde in als de dropdown wordt geopend
|
||||
$('#timezone').on('select2:open', function() {
|
||||
if ($(this).val()) {
|
||||
setTimeout(function() {
|
||||
let selectedOption = $('.select2-results__option[aria-selected=true]');
|
||||
if (selectedOption.length) {
|
||||
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
75
eveai_app/templates/user/edit_consent_version.html
Normal file
75
eveai_app/templates/user/edit_consent_version.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field, render_included_field %}
|
||||
|
||||
{% block title %}Create or Edit Consent Version{% endblock %}
|
||||
|
||||
{% block content_title %}Create or Edit Consent Version{% endblock %}
|
||||
{% block content_description %}Create or Edit Consent Version{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = ["consent_type"] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Consent Version</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// JavaScript om de gebruiker's timezone te detecteren
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
// Detect timezone
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Send timezone to the server via a POST request
|
||||
fetch('set_user_timezone', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ timezone: userTimezone })
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
console.log('Timezone sent to server successfully');
|
||||
} else {
|
||||
console.error('Failed to send timezone to server');
|
||||
}
|
||||
});
|
||||
|
||||
$('#timezone').select2({
|
||||
placeholder: 'Selecteer een timezone...',
|
||||
allowClear: true,
|
||||
theme: 'bootstrap',
|
||||
width: '100%',
|
||||
dropdownAutoWidth: true,
|
||||
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
|
||||
scrollAfterSelect: false,
|
||||
// Verbeterd scroll gedrag
|
||||
dropdownParent: $('body')
|
||||
});
|
||||
|
||||
// Stel de huidige waarde in als de dropdown wordt geopend
|
||||
$('#timezone').on('select2:open', function() {
|
||||
if ($(this).val()) {
|
||||
setTimeout(function() {
|
||||
let selectedOption = $('.select2-results__option[aria-selected=true]');
|
||||
if (selectedOption.length) {
|
||||
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
15
eveai_app/templates/user/no_consent.html
Normal file
15
eveai_app/templates/user/no_consent.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Consent Required{% endblock %}
|
||||
{% block content_title %}Consent Required{% endblock %}
|
||||
{% block content_description %}Access is restricted until required consents are provided{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="alert alert-danger">You currently do not have access because required consents are missing or expired.</div>
|
||||
<p>Please contact your Tenant Admin or Management Partner to review and accept the latest Data Privacy Agreement and Terms & Conditions.</p>
|
||||
{% if current_user.has_roles('Tenant Admin', 'Partner Admin', 'Super User') %}
|
||||
<a href="{{ prefixed_url_for('user_bp.tenant_consent') }}" class="btn btn-primary">Go to Tenant Consents</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{# HTML fragment to render consent markdown content #}
|
||||
<div class="markdown-body">
|
||||
{{ markdown_content | markdown }}
|
||||
</div>
|
||||
64
eveai_app/templates/user/tenant_consent.html
Normal file
64
eveai_app/templates/user/tenant_consent.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import debug_to_console %}
|
||||
|
||||
{% block title %}Tenant Consents{% endblock %}
|
||||
{% block content_title %}Tenant Consents{% endblock %}
|
||||
{% block content_description %}Please consent if required before continuing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% if statuses %}
|
||||
<div class="row">
|
||||
{% for s in statuses %}
|
||||
<div class="col-12 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ s.consent_type }}</h5>
|
||||
<p class="card-text">
|
||||
Status: <span class="badge {% if s.status == 'CONSENTED' %}bg-success{% elif s.status == 'RENEWAL_REQUIRED' %}bg-warning text-dark{% else %}bg-danger{% endif %}">{{ s.status }}</span>
|
||||
</p>
|
||||
<p class="card-text small text-muted">Active version: {{ s.active_version or 'n/a' }}, Last accepted: {{ s.last_version or 'n/a' }}</p>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-view-consent"
|
||||
data-consent-type="{{ s.consent_type }}" data-version="{{ s.active_version }}"
|
||||
data-url="{{ prefixed_url_for('user_bp.view_consent_markdown', consent_type=s.consent_type, version=s.active_version) }}"
|
||||
{% if not s.active_version %}disabled{% endif %}>
|
||||
Bekijk document
|
||||
</button>
|
||||
{% if s.status != 'CONSENTED' %}
|
||||
<form method="post" action="{{ prefixed_url_for('user_bp.accept_tenant_consent', tenant_id=tenant_id, consent_type=s.consent_type) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary">I agree</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-success align-self-center">Up to date</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">No consent status information available.</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Consent document viewer moved into main content -->
|
||||
<div class="container mt-4" id="consent-viewer-section" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Document viewer:</strong>
|
||||
<span id="viewer-type"></span>
|
||||
<span class="text-muted">version</span>
|
||||
<span id="viewer-version"></span>
|
||||
</div>
|
||||
<div id="viewer-loading" class="text-muted" style="display:none;">Loading...</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="consent-document-viewer" class="markdown-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
eveai_app/templates/user/tenant_consent_renewal.html
Normal file
60
eveai_app/templates/user/tenant_consent_renewal.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Consent Renewal{% endblock %}
|
||||
{% block content_title %}Consent Renewal{% endblock %}
|
||||
{% block content_description %}Renew consents that require attention{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% if statuses and statuses|length > 0 %}
|
||||
<div class="alert alert-warning">Some consents need renewal. Please review and accept the latest version.</div>
|
||||
<div class="row">
|
||||
{% for s in statuses %}
|
||||
<div class="col-12 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ s.consent_type }}</h5>
|
||||
<p class="card-text">
|
||||
Status: <span class="badge {% if s.status == 'RENEWAL_REQUIRED' %}bg-warning text-dark{% else %}bg-danger{% endif %}">{{ s.status }}</span>
|
||||
</p>
|
||||
<p class="card-text small text-muted">Active version: {{ s.active_version or 'n/a' }}, Last accepted: {{ s.last_version or 'n/a' }}</p>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-view-consent"
|
||||
data-consent-type="{{ s.consent_type }}" data-version="{{ s.active_version }}"
|
||||
data-url="{{ prefixed_url_for('user_bp.view_consent_markdown', consent_type=s.consent_type, version=s.active_version) }}"
|
||||
{% if not s.active_version %}disabled{% endif %}>
|
||||
Bekijk document
|
||||
</button>
|
||||
<form method="post" action="{{ prefixed_url_for('user_bp.accept_tenant_consent', tenant_id=tenant_id, consent_type=s.consent_type) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary">Renew and accept</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">All consents are up to date.</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Consent document viewer moved into main content -->
|
||||
<div class="container mt-4" id="consent-viewer-section" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Document viewer:</strong>
|
||||
<span id="viewer-type"></span>
|
||||
<span class="text-muted">version</span>
|
||||
<span id="viewer-version"></span>
|
||||
</div>
|
||||
<div id="viewer-loading" class="text-muted" style="display:none;">Loading...</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="consent-document-viewer" class="markdown-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
eveai_app/templates/user/tenant_consents_overview.html
Normal file
59
eveai_app/templates/user/tenant_consents_overview.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Tenant Consents Overview{% endblock %}
|
||||
{% block content_title %}Tenant Consents Overview{% endblock %}
|
||||
{% block content_description %}Manage consents for this tenant{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% for s in statuses %}
|
||||
<div class="col-12 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ s.consent_type }}</h5>
|
||||
<p class="card-text">
|
||||
Status: <span class="badge {% if s.status == 'CONSENTED' %}bg-success{% elif s.status == 'RENEWAL_REQUIRED' %}bg-warning text-dark{% else %}bg-danger{% endif %}">{{ s.status }}</span>
|
||||
</p>
|
||||
<p class="card-text small text-muted">Active version: {{ s.active_version or 'n/a' }}, Last accepted: {{ s.last_version or 'n/a' }}</p>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-view-consent"
|
||||
data-consent-type="{{ s.consent_type }}" data-version="{{ s.active_version }}"
|
||||
data-url="{{ prefixed_url_for('user_bp.view_consent_markdown', consent_type=s.consent_type, version=s.active_version) }}"
|
||||
{% if not s.active_version %}disabled{% endif %}>
|
||||
Bekijk document
|
||||
</button>
|
||||
{% if s.status != 'CONSENTED' %}
|
||||
<form method="post" action="{{ prefixed_url_for('user_bp.accept_tenant_consent', tenant_id=tenant_id, consent_type=s.consent_type) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary">Accept on behalf</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-success align-self-center">Up to date</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Consent document viewer moved into main content -->
|
||||
<div class="container mt-4" id="consent-viewer-section" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Document viewer:</strong>
|
||||
<span id="viewer-type"></span>
|
||||
<span class="text-muted">version</span>
|
||||
<span id="viewer-version"></span>
|
||||
</div>
|
||||
<div id="viewer-loading" class="text-muted" style="display:none;">Loading...</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="consent-document-viewer" class="markdown-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -118,7 +118,10 @@ def view_content(content_type):
|
||||
Show content like release notes, terms of use, etc.
|
||||
|
||||
Args:
|
||||
content_type (str): Type content (eg. 'changelog', 'terms', 'privacy')
|
||||
content_type (str): Type content (eg. 'changelog', 'terms', 'dpa')
|
||||
Request Args:
|
||||
version (str): Major.Minor version of the content to display
|
||||
patch (str): Patch version of the content to display
|
||||
"""
|
||||
try:
|
||||
major_minor = request.args.get('version')
|
||||
@@ -135,14 +138,14 @@ def view_content(content_type):
|
||||
titles = {
|
||||
'changelog': 'Release Notes',
|
||||
'terms': 'Terms & Conditions',
|
||||
'privacy': 'Privacy Statement',
|
||||
'dpa': 'Data Privacy Agreement',
|
||||
# Voeg andere types toe indien nodig
|
||||
}
|
||||
|
||||
descriptions = {
|
||||
'changelog': 'EveAI Release Notes',
|
||||
'terms': "Terms & Conditions for using AskEveAI's Evie",
|
||||
'privacy': "Privacy Statement for AskEveAI's Evie",
|
||||
'dpa': "Data Privacy Agreement for AskEveAI's Evie",
|
||||
# Voeg andere types toe indien nodig
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ from flask_security import roles_accepted
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import ast
|
||||
|
||||
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService
|
||||
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService, \
|
||||
ConsentVersion, TenantConsent, Partner
|
||||
from common.services.user import UserServices, PartnerServices
|
||||
from common.utils.eveai_exceptions import EveAINoSessionPartner, EveAINoManagementPartnerService
|
||||
from common.utils.security_utils import current_user_has_role
|
||||
from eveai_app.views.list_views.list_view_utils import render_list_view
|
||||
from common.extensions import db
|
||||
|
||||
# Tenant list view helper
|
||||
def get_tenants_list_view():
|
||||
@@ -287,6 +289,8 @@ def get_tenant_makes_list_view(tenant_id):
|
||||
|
||||
|
||||
# Tenant Partner Services list view helper
|
||||
|
||||
|
||||
def get_tenant_partner_services_list_view(tenant_id):
|
||||
"""Generate the tenant partner services list view configuration for a specific tenant"""
|
||||
# Get partner services for the tenant through PartnerTenant association
|
||||
@@ -328,3 +332,108 @@ def get_tenant_partner_services_list_view(tenant_id):
|
||||
'form_action': url_for('user_bp.tenant_partner_services'),
|
||||
'description': f'Partner Services for tenant {tenant_id}'
|
||||
}
|
||||
|
||||
|
||||
def get_consent_versions_list_view():
|
||||
"""Generate the tenant makes list view configuration for a specific tenant"""
|
||||
# Get makes for the tenant
|
||||
query = ConsentVersion.query.filter_by().order_by(ConsentVersion.id)
|
||||
consent_versions = query.all()
|
||||
|
||||
# Prepare data for Tabulator
|
||||
data = []
|
||||
for cv in consent_versions:
|
||||
data.append({
|
||||
'id': cv.id,
|
||||
'consent_type': cv.consent_type,
|
||||
'consent_version': cv.consent_version,
|
||||
'consent_valid_from': cv.consent_valid_from.strftime('%Y-%m-%d') if cv.consent_valid_from else '',
|
||||
'consent_valid_to': cv.consent_valid_to.strftime('%Y-%m-%d') if cv.consent_valid_to else '',
|
||||
})
|
||||
|
||||
# Column Definitions
|
||||
columns = [
|
||||
{'title': 'ID', 'field': 'id', 'width': 80},
|
||||
{'title': 'Consent Type', 'field': 'consent_type'},
|
||||
{'title': 'From', 'field': 'consent_valid_from'},
|
||||
{'title': 'To', 'field': 'consent_valid_to'}
|
||||
]
|
||||
|
||||
actions = [
|
||||
{'value': 'edit_consent_version', 'text': 'Edit Consent Version', 'class': 'btn-primary', 'requiresSelection': True},
|
||||
{'value': 'create_consent_version', 'text': 'Create Consent Version', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
|
||||
]
|
||||
|
||||
initial_sort = [{'column': 'id', 'dir': 'asc'}]
|
||||
|
||||
return {
|
||||
'title': 'Consent Versions',
|
||||
'data': data,
|
||||
'columns': columns,
|
||||
'actions': actions,
|
||||
'initial_sort': initial_sort,
|
||||
'table_id': 'consent_versions_table',
|
||||
'form_action': url_for('user_bp.handle_consent_version_selection'),
|
||||
'description': f'Consent Versions'
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Tenant Consents history list view helper
|
||||
|
||||
def get_tenant_consents_list_view(tenant_id):
|
||||
"""Generate the tenant consents history list view configuration for a specific tenant"""
|
||||
# Build query joining optional partner->tenant for partner name and user for user name
|
||||
q = db.session.query(
|
||||
TenantConsent.id,
|
||||
TenantConsent.consent_type,
|
||||
TenantConsent.consent_date,
|
||||
TenantConsent.consent_version,
|
||||
User.user_name.label('user_name'),
|
||||
Tenant.name.label('partner_name')
|
||||
).join(User, User.id == TenantConsent.user_id)
|
||||
# Left join Partner and its Tenant to get the partner tenant name
|
||||
q = q.outerjoin(Partner, Partner.id == TenantConsent.partner_id)
|
||||
q = q.outerjoin(Tenant, Tenant.id == Partner.tenant_id)
|
||||
q = q.filter(TenantConsent.tenant_id == tenant_id).order_by(TenantConsent.consent_date.desc())
|
||||
|
||||
rows = q.all()
|
||||
|
||||
data = []
|
||||
for r in rows:
|
||||
partner_name = r.partner_name if r.partner_name else '-'
|
||||
data.append({
|
||||
'id': r.id,
|
||||
'consent_type': r.consent_type,
|
||||
'consent_date': r.consent_date.isoformat() if hasattr(r.consent_date, 'isoformat') else str(r.consent_date),
|
||||
'consent_version': r.consent_version,
|
||||
'user_name': r.user_name,
|
||||
'partner_name': partner_name,
|
||||
})
|
||||
|
||||
columns = [
|
||||
{'title': 'ID', 'field': 'id', 'width': 80},
|
||||
{'title': 'Type', 'field': 'consent_type'},
|
||||
{'title': 'Date', 'field': 'consent_date'},
|
||||
{'title': 'Version', 'field': 'consent_version'},
|
||||
{'title': 'User', 'field': 'user_name'},
|
||||
{'title': 'Partner', 'field': 'partner_name'},
|
||||
]
|
||||
|
||||
# Only a view action as records are immutable; handled via a POST -> redirect in the view handler
|
||||
actions = [
|
||||
{'value': 'view_consent_document', 'text': 'Bekijk document', 'class': 'btn-outline-secondary', 'requiresSelection': True},
|
||||
]
|
||||
|
||||
initial_sort = [{'column': 'consent_date', 'dir': 'desc'}]
|
||||
|
||||
return {
|
||||
'title': 'Tenant Consents History',
|
||||
'data': data,
|
||||
'columns': columns,
|
||||
'actions': actions,
|
||||
'initial_sort': initial_sort,
|
||||
'table_id': 'tenant_consents_history_table',
|
||||
'form_action': url_for('user_bp.handle_tenant_consents_history_selection'),
|
||||
'description': f'Consent history for tenant {tenant_id}'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# views/security_views.py
|
||||
from flask import Blueprint, render_template, redirect, request, flash, current_app, abort, session
|
||||
from flask import Blueprint, render_template, redirect, request, flash, current_app, abort, session, jsonify
|
||||
from flask_security import current_user, login_required, login_user, logout_user
|
||||
from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
|
||||
from flask_security.forms import LoginForm
|
||||
@@ -10,7 +10,8 @@ from datetime import datetime as dt, timezone as tz
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.models.user import User
|
||||
from common.models.user import User, ConsentStatus
|
||||
from common.services.user import TenantServices, UserServices
|
||||
from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, ForgotPasswordForm
|
||||
@@ -56,7 +57,9 @@ def login():
|
||||
db.session.commit()
|
||||
if current_user.has_roles('Super User'):
|
||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||
else:
|
||||
if current_user.has_roles('Partner Admin'):
|
||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||
# After login, rely on global consent guard; just go to default start
|
||||
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
|
||||
else:
|
||||
flash('Invalid username or password', 'danger')
|
||||
@@ -143,6 +146,102 @@ def reset_password(token):
|
||||
return render_template('security/reset_password.html', reset_password_form=form)
|
||||
|
||||
|
||||
@security_bp.route('/consent/sign', methods=['POST'])
|
||||
@login_required
|
||||
def consent_sign():
|
||||
try:
|
||||
# Determine tenant context
|
||||
tenant_id = None
|
||||
# Payload may provide a tenant_id for admins signing for others
|
||||
if request.is_json:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
tenant_id = payload.get('tenant_id')
|
||||
consent_data = payload.get('consent_data', {})
|
||||
else:
|
||||
tenant_id = request.form.get('tenant_id')
|
||||
consent_data = {}
|
||||
if tenant_id is None:
|
||||
# default to user's tenant (Tenant Admin)
|
||||
tenant_id = current_user.tenant_id
|
||||
tenant_id = int(tenant_id)
|
||||
|
||||
# Authorization
|
||||
allowed = False
|
||||
if current_user.has_roles('Super User'):
|
||||
allowed = True
|
||||
elif current_user.has_roles('Partner Admin') and UserServices.can_user_edit_tenant(tenant_id):
|
||||
allowed = True
|
||||
elif current_user.has_roles('Tenant Admin') and getattr(current_user, 'tenant_id', None) == tenant_id:
|
||||
allowed = True
|
||||
if not allowed:
|
||||
abort(403)
|
||||
|
||||
# Determine consent versions/types to record
|
||||
cts = current_app.config.get('CONSENT_TYPES', [])
|
||||
from common.models.user import TenantConsent, ConsentVersion, PartnerService
|
||||
from common.services.user.partner_services import PartnerServices
|
||||
|
||||
# Resolve partner and management service if available in session (for Partner Admin)
|
||||
partner_id = None
|
||||
partner_service_id = None
|
||||
try:
|
||||
if 'partner' in session and session['partner'].get('services'):
|
||||
partner_id = session['partner'].get('id')
|
||||
mgmt = PartnerServices.get_management_service()
|
||||
if mgmt:
|
||||
partner_service_id = mgmt.get('id')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallbacks if not Partner Admin context
|
||||
if partner_id is None:
|
||||
# Try find partner by tenant (one-to-one in model)
|
||||
from common.models.user import Partner, Tenant
|
||||
t = Tenant.query.get(tenant_id)
|
||||
if t and t.partner:
|
||||
partner_id = t.partner.id
|
||||
if partner_service_id is None and partner_id is not None:
|
||||
ps = PartnerService.query.filter_by(partner_id=partner_id, type='MANAGEMENT_SERVICE').first()
|
||||
if ps:
|
||||
partner_service_id = ps.id
|
||||
|
||||
# For each consent type, record acceptance of latest version
|
||||
now = dt.now(tz.utc)
|
||||
for ct in cts:
|
||||
cv = ConsentVersion.query.filter_by(consent_type=ct).order_by(ConsentVersion.consent_valid_from.desc()).first()
|
||||
if not cv:
|
||||
current_app.logger.error(f'No ConsentVersion found for type {ct}; skipping')
|
||||
continue
|
||||
tc = TenantConsent(
|
||||
tenant_id=tenant_id,
|
||||
partner_id=partner_id or 0,
|
||||
partner_service_id=partner_service_id or 0,
|
||||
user_id=current_user.id,
|
||||
consent_type=ct,
|
||||
consent_version=cv.consent_version,
|
||||
consent_data=consent_data or {}
|
||||
)
|
||||
db.session.add(tc)
|
||||
db.session.commit()
|
||||
|
||||
if request.is_json or 'application/json' in request.headers.get('Accept', ''):
|
||||
return jsonify({'ok': True, 'tenant_id': tenant_id}), 200
|
||||
# Default UX: go to overview
|
||||
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
|
||||
except CSRFError:
|
||||
if request.is_json:
|
||||
return jsonify({'ok': False, 'error': 'csrf_error'}), 400
|
||||
flash('Session expired. Please retry.', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Consent signing failed: {e}')
|
||||
db.session.rollback()
|
||||
if request.is_json:
|
||||
return jsonify({'ok': False, 'error': str(e)}), 400
|
||||
flash('Failed to sign consent.', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ class TenantMakeForm(DynamicFormBase):
|
||||
self.allowed_languages.choices = [(details['iso 639-1'], f"{details['flag']} {details['iso 639-1']}")
|
||||
for name, details in lang_details.items()]
|
||||
|
||||
|
||||
class EditTenantMakeForm(DynamicFormBase):
|
||||
id = IntegerField('ID', widget=HiddenInput())
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
|
||||
@@ -212,5 +213,21 @@ class EditTenantMakeForm(DynamicFormBase):
|
||||
self.default_language.choices = choices
|
||||
|
||||
|
||||
class ConsentVersionForm(FlaskForm):
|
||||
consent_type = SelectField('Consent Type', choices=[], validators=[DataRequired()])
|
||||
consent_version = StringField('Consent Version', validators=[DataRequired(), Length(max=20)])
|
||||
consent_valid_from = DateField('Consent Valid From', id='form-control datepicker', validators=[DataRequired()])
|
||||
consent_valid_to = DateField('Consent Valid To', id='form-control datepicker', validators=[Optional()])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ConsentVersionForm, self).__init__(*args, **kwargs)
|
||||
# Initialise consent types
|
||||
self.consent_type.choices = [(t, t) for t in current_app.config['CONSENT_TYPES']]
|
||||
|
||||
|
||||
class EditConsentVersionForm(FlaskForm):
|
||||
consent_type = StringField('Consent Type', validators=[DataRequired()])
|
||||
consent_version = StringField('Consent Version', validators=[DataRequired(), Length(max=20)])
|
||||
consent_valid_from = DateField('Consent Valid From', id='form-control datepicker', validators=[DataRequired()])
|
||||
consent_valid_to = DateField('Consent Valid To', id='form-control datepicker', validators=[Optional()])
|
||||
|
||||
|
||||
@@ -6,13 +6,18 @@ from flask_security import roles_accepted, current_user
|
||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||
import ast
|
||||
|
||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake
|
||||
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
|
||||
from wtforms import BooleanField
|
||||
|
||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake, \
|
||||
ConsentVersion, TenantConsent
|
||||
from common.extensions import db, security, minio_client, simple_encryption, cache_manager, content_manager
|
||||
from common.services.utils.version_services import VersionServices
|
||||
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||
from config.type_defs.service_types import SERVICE_TYPES
|
||||
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
||||
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
|
||||
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm, ConsentVersionForm, \
|
||||
EditConsentVersionForm
|
||||
from common.utils.database import Database
|
||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||
from common.utils.simple_encryption import generate_api_key
|
||||
@@ -25,8 +30,10 @@ from common.utils.mail_utils import send_email
|
||||
|
||||
from eveai_app.views.list_views.user_list_views import get_tenants_list_view, get_users_list_view, \
|
||||
get_tenant_domains_list_view, get_tenant_projects_list_view, get_tenant_makes_list_view, \
|
||||
get_tenant_partner_services_list_view
|
||||
get_tenant_partner_services_list_view, get_consent_versions_list_view, get_tenant_consents_list_view
|
||||
from eveai_app.views.list_views.list_view_utils import render_list_view
|
||||
from common.services.user.consent_services import ConsentServices
|
||||
from common.models.user import ConsentStatus
|
||||
|
||||
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
||||
|
||||
@@ -693,6 +700,209 @@ def tenant_partner_services():
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
# Consent Version Management ----------------------------------------------------------------------
|
||||
@user_bp.route('/consent_versions', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User')
|
||||
def consent_versions():
|
||||
config = get_consent_versions_list_view()
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@user_bp.route('/handle_consent_version_selection', methods=['POST'])
|
||||
@roles_accepted('Super User')
|
||||
def handle_consent_version_selection():
|
||||
action = request.form['action']
|
||||
if action == 'create_consent_version':
|
||||
return redirect(prefixed_url_for('user_bp.consent_version', for_redirect=True))
|
||||
consent_version_identification = request.form.get('selected_row')
|
||||
consent_version_id = ast.literal_eval(consent_version_identification).get('value')
|
||||
|
||||
if action == 'edit_consent_version':
|
||||
return redirect(prefixed_url_for('user_bp.edit_consent_version', consent_version_id=consent_version_id, for_redirect=True))
|
||||
|
||||
# Altijd teruggaan naar de tenant_makes pagina
|
||||
return redirect(prefixed_url_for('user_bp.consent_versions', for_redirect=True))
|
||||
|
||||
|
||||
@user_bp.route('/consent_version', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User')
|
||||
def consent_version():
|
||||
form = ConsentVersionForm()
|
||||
if form.validate_on_submit():
|
||||
new_consent_version = ConsentVersion()
|
||||
form.populate_obj(new_consent_version)
|
||||
set_logging_information(new_consent_version, dt.now(tz.utc))
|
||||
|
||||
try:
|
||||
db.session.add(new_consent_version)
|
||||
db.session.commit()
|
||||
flash('Consent Version successfully added!', 'success')
|
||||
current_app.logger.info(f'Consent Version {new_consent_version.consent_type}, version {new_consent_version.consent_version} successfully added ')
|
||||
# Enable step 2 of creation of retriever - add configuration of the retriever (dependent on type)
|
||||
return redirect(prefixed_url_for('user_bp.consent_versions', for_redirect=True))
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to add Consent Version. Error: {e}', 'danger')
|
||||
current_app.logger.error(f'Failed to add Consent Version. Error: {str(e)}')
|
||||
|
||||
return render_template('user/consent_version.html', form=form)
|
||||
|
||||
|
||||
@user_bp.route('/consent_version/<int:consent_version_id>', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User')
|
||||
def edit_consent_version(consent_version_id):
|
||||
"""Edit an existing Consent Version."""
|
||||
# Get the Consent Version or return 404
|
||||
cv = ConsentVersion.query.get_or_404(consent_version_id)
|
||||
|
||||
# Create form instance with the tenant make
|
||||
form = EditConsentVersionForm(request.form, obj=cv)
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Update basic fields
|
||||
form.populate_obj(cv)
|
||||
# Update logging information
|
||||
update_logging_information(cv, dt.now(tz.utc))
|
||||
|
||||
# Save changes to database
|
||||
try:
|
||||
db.session.add(cv)
|
||||
db.session.commit()
|
||||
flash('Consent Version updated successfully!', 'success')
|
||||
current_app.logger.info(f'Consent Version {cv.id} updated successfully')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update Consent Version. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f'Failed to update Consent Version {consent_version_id}. Error: {str(e)}')
|
||||
return render_template('user/consent_version.html', form=form, consent_version_id=consent_version_id)
|
||||
|
||||
return redirect(prefixed_url_for('user_bp.consent_versions', for_redirect=True))
|
||||
else:
|
||||
form_validation_failed(request, form)
|
||||
|
||||
return render_template('user/edit_consent_version.html', form=form, consent_version_id=consent_version_id)
|
||||
|
||||
|
||||
# Tenant Consent Management -----------------------------------------------------------------------
|
||||
@user_bp.route('/consent/tenant', methods=['GET'])
|
||||
@roles_accepted('Tenant Admin')
|
||||
def tenant_consent():
|
||||
# Overview for current session tenant
|
||||
tenant_id = session.get('tenant', {}).get('id') or getattr(current_user, 'tenant_id', None)
|
||||
if not tenant_id:
|
||||
flash('No tenant context.', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||
types = ConsentServices.get_required_consent_types()
|
||||
statuses = [ConsentServices.evaluate_type_status(tenant_id, t) for t in types]
|
||||
if current_app.jinja_env.loader:
|
||||
return render_template('user/tenant_consent.html', statuses=statuses, tenant_id=tenant_id)
|
||||
# Fallback text if no templates
|
||||
lines = [f"{s.consent_type}: {s.status} (active={s.active_version}, last={s.last_version})" for s in statuses]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@user_bp.route('/consent/no_access', methods=['GET'])
|
||||
def no_consent():
|
||||
return render_template('user/no_consent.html') if current_app.jinja_env.loader else "Consent required - contact your admin"
|
||||
|
||||
|
||||
@user_bp.route('/consent/tenant_renewal', methods=['GET'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def tenant_consent_renewal():
|
||||
# Show renewal statuses only
|
||||
tenant_id = session.get('tenant', {}).get('id') or getattr(current_user, 'tenant_id', None)
|
||||
types = ConsentServices.get_required_consent_types()
|
||||
statuses = [s for s in [ConsentServices.evaluate_type_status(tenant_id, t) for t in types] if s.status != ConsentStatus.CONSENTED]
|
||||
if current_app.jinja_env.loader:
|
||||
return render_template('user/tenant_consent_renewal.html', statuses=statuses, tenant_id=tenant_id)
|
||||
return "\n".join([f"{s.consent_type}: {s.status}" for s in statuses])
|
||||
|
||||
|
||||
@user_bp.route('/consent/renewal', methods=['GET'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def consent_renewal():
|
||||
return render_template('user/consent_renewal.html') if current_app.jinja_env.loader else "Consent renewal in progress"
|
||||
|
||||
|
||||
@user_bp.route('/tenants/<int:tenant_id>/consents', methods=['GET'])
|
||||
@roles_accepted('Super User', 'Partner Admin')
|
||||
def view_tenant_consents(tenant_id: int):
|
||||
# Authorization: Tenant Admin for own tenant or Management Partner
|
||||
allowed, mode, _, _ = ConsentServices.can_consent_on_behalf(tenant_id)
|
||||
if not (allowed or current_user.has_roles('Super User')):
|
||||
flash('Not authorized to view consents for this tenant', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
|
||||
types = ConsentServices.get_required_consent_types()
|
||||
statuses = [ConsentServices.evaluate_type_status(tenant_id, t) for t in types]
|
||||
if current_app.jinja_env.loader:
|
||||
return render_template('user/tenant_consents_overview.html', statuses=statuses, tenant_id=tenant_id)
|
||||
return "\n".join([f"{s.consent_type}: {s.status}" for s in statuses])
|
||||
|
||||
|
||||
@user_bp.route('/tenants/<int:tenant_id>/consents/<string:consent_type>/accept', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def accept_tenant_consent(tenant_id: int, consent_type: str):
|
||||
try:
|
||||
tc = ConsentServices.record_consent(tenant_id, consent_type)
|
||||
flash(f"Consent for {consent_type} recorded (version {tc.consent_version})", 'success')
|
||||
except PermissionError:
|
||||
flash('Not authorized to accept this consent for the tenant', 'danger')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to record consent: {e}")
|
||||
flash('Failed to record consent', 'danger')
|
||||
if current_user.has_roles('Tenant Admin'):
|
||||
return redirect(prefixed_url_for('user_bp.tenant_consent', for_redirect=True))
|
||||
else:
|
||||
return redirect(prefixed_url_for('user_bp.view_tenant_consents', tenant_id=tenant_id, for_redirect=True))
|
||||
|
||||
|
||||
@user_bp.route('/consents/<path:consent_type>/<string:version>/view', methods=['GET'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def view_consent_markdown(consent_type: str, version: str):
|
||||
"""Render the consent markdown for a given type and version as an HTML fragment, using content_manager."""
|
||||
try:
|
||||
current_app.logger.debug(f"Rendering markdown for {consent_type} version {version}")
|
||||
# Validate type against config
|
||||
valid_types = set(ConsentServices.get_required_consent_types())
|
||||
if consent_type not in valid_types:
|
||||
for t in valid_types:
|
||||
if t.lower() == consent_type.lower():
|
||||
consent_type = t
|
||||
break
|
||||
if consent_type not in valid_types:
|
||||
current_app.logger.warning(f"Unknown consent type requested for view: {consent_type}")
|
||||
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Unknown consent type: {consent_type}"), 404)
|
||||
|
||||
# Version must exist in ConsentVersion for the type
|
||||
cv = ConsentVersion.query.filter_by(consent_type=consent_type, consent_version=version).first()
|
||||
if not cv:
|
||||
current_app.logger.warning(f"Unknown consent version requested: type={consent_type}, version={version}")
|
||||
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Document not found for version {version}"), 404)
|
||||
|
||||
# Map consent type to content_manager content_type
|
||||
type_map = current_app.config.get('CONSENT_TYPE_MAP', {})
|
||||
content_type = type_map.get(consent_type)
|
||||
if not content_type:
|
||||
current_app.logger.warning(f"No content_type mapping for consent type {consent_type}")
|
||||
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content=f"Unknown content mapping for {consent_type}"), 404)
|
||||
|
||||
# Parse major.minor and patch from version (e.g., 1.2.3 -> 1.2 and 1.2.3)
|
||||
major_minor, patch = VersionServices.split_version(version)
|
||||
|
||||
# Use content_manager to read content
|
||||
content_data = content_manager.read_content(content_type, major_minor, patch)
|
||||
if not content_data or not content_data.get('content'):
|
||||
markdown_content = f"# Document not found\nThe consent document for {consent_type} version {version} could not be located."
|
||||
status = 404
|
||||
else:
|
||||
markdown_content = content_data['content']
|
||||
status = 200
|
||||
|
||||
return render_template('user/partials/consent_markdown_fragment.html', markdown_content=markdown_content), status
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error in view_consent_markdown: {e}")
|
||||
return (render_template('user/partials/consent_markdown_fragment.html', markdown_content="Unexpected error rendering document."), 500)
|
||||
|
||||
|
||||
def reset_uniquifier(user):
|
||||
security.datastore.set_uniquifier(user)
|
||||
@@ -755,3 +965,41 @@ def send_api_key_notification(tenant_id, tenant_name, project_name, api_key, ser
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to send API key notification email: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@user_bp.route('/tenant_consents_history', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def tenant_consents_history():
|
||||
tenant_id = session['tenant']['id']
|
||||
config = get_tenant_consents_list_view(tenant_id)
|
||||
return render_list_view('list_view.html', **config)
|
||||
|
||||
|
||||
@user_bp.route('/handle_tenant_consents_history_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||
def handle_tenant_consents_history_selection():
|
||||
action = request.form.get('action')
|
||||
if action == 'view_consent_document':
|
||||
tenant_consent_identification = request.form.get('selected_row')
|
||||
if not tenant_consent_identification:
|
||||
flash('No consent selected', 'warning')
|
||||
return redirect(prefixed_url_for('user_bp.tenant_consents_history', for_redirect=True))
|
||||
try:
|
||||
consent_id = ast.literal_eval(tenant_consent_identification).get('value')
|
||||
except Exception:
|
||||
flash('Invalid selection', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.tenant_consents_history', for_redirect=True))
|
||||
tc = TenantConsent.query.get_or_404(consent_id)
|
||||
type_map = current_app.config.get('CONSENT_TYPE_MAP', {})
|
||||
consent_type_dir = type_map.get(tc.consent_type)
|
||||
major_minor, patch = VersionServices.split_version(tc.consent_version)
|
||||
# Redirect to the fragment view; the template will render the fragment response as a full page if opened
|
||||
return redirect(prefixed_url_for(
|
||||
'basic_bp.view_content',
|
||||
content_type=consent_type_dir,
|
||||
version=major_minor,
|
||||
patch=patch,
|
||||
for_redirect=True
|
||||
))
|
||||
# Default: back to the history page
|
||||
return redirect(prefixed_url_for('user_bp.tenant_consents_history', for_redirect=True))
|
||||
|
||||
@@ -110,7 +110,7 @@ export function useContentModal() {
|
||||
throw new Error(data.error || 'Onbekende fout bij het laden van content');
|
||||
}
|
||||
} else if (data.content !== undefined) {
|
||||
// Legacy format without success property (current privacy/terms endpoints)
|
||||
// Legacy format without success property (current dpa/terms endpoints)
|
||||
modalState.content = data.content || '';
|
||||
modalState.version = data.version || '';
|
||||
} else if (data.error) {
|
||||
|
||||
@@ -24,19 +24,19 @@ export default {
|
||||
props: {
|
||||
template: { type: String, required: true },
|
||||
asButton: { type: Boolean, default: false },
|
||||
ariaPrivacy: { type: String, default: 'Open privacy statement in a dialog' },
|
||||
ariaTerms: { type: String, default: 'Open terms and conditions in a dialog' }
|
||||
ariaPrivacy: { type: String, default: 'Open Data Privacy Agreement in a dialog' },
|
||||
ariaTerms: { type: String, default: 'Open Terms and Conditions in a dialog' }
|
||||
},
|
||||
emits: ['open-privacy', 'open-terms'],
|
||||
emits: ['open-dpa', 'open-terms'],
|
||||
computed: {
|
||||
linkTag() {
|
||||
return this.asButton ? 'button' : 'a';
|
||||
},
|
||||
nodes() {
|
||||
// Parse only allowed tags <privacy>...</privacy> and <terms>...</terms>
|
||||
// Parse only allowed tags <dpa>...</dpa> and <terms>...</terms>
|
||||
const source = (this.template || '');
|
||||
|
||||
// 2) parse only allowed tags <privacy>...</privacy> and <terms>...</terms>
|
||||
// 2) parse only allowed tags <dpa>...</dpa> and <terms>...</terms>
|
||||
const pattern = /<(privacy|terms)>([\s\S]*?)<\/\1>/gi;
|
||||
const out = [];
|
||||
let lastIndex = 0;
|
||||
@@ -48,9 +48,9 @@ export default {
|
||||
out.push({ type: 'text', text: source.slice(lastIndex, start) });
|
||||
}
|
||||
out.push({
|
||||
type: tag, // 'privacy' | 'terms'
|
||||
type: tag, // 'dpa' | 'terms'
|
||||
label: (label || '').trim(),
|
||||
aria: tag === 'privacy' ? this.ariaPrivacy : this.ariaTerms
|
||||
aria: tag === 'dpa' ? this.ariaPrivacy : this.ariaTerms
|
||||
});
|
||||
lastIndex = start + full.length;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
emitClick(kind) {
|
||||
if (kind === 'privacy') this.$emit('open-privacy');
|
||||
if (kind === 'dpa') this.$emit('open-dpa');
|
||||
if (kind === 'terms') this.$emit('open-terms');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@ export default {
|
||||
|
||||
// Modal handling methods
|
||||
openPrivacyModal() {
|
||||
this.loadContent('privacy');
|
||||
this.loadContent('dpa');
|
||||
},
|
||||
|
||||
openTermsModal() {
|
||||
@@ -494,15 +494,15 @@ export default {
|
||||
retryLoad() {
|
||||
// Retry loading the last requested content type
|
||||
const currentTitle = this.contentModal.modalState.title.toLowerCase();
|
||||
if (currentTitle.includes('privacy')) {
|
||||
this.loadContent('privacy');
|
||||
if (currentTitle.includes('dpa')) {
|
||||
this.loadContent('dpa');
|
||||
} else if (currentTitle.includes('terms')) {
|
||||
this.loadContent('terms');
|
||||
}
|
||||
},
|
||||
|
||||
async loadContent(contentType) {
|
||||
const title = contentType === 'privacy' ? 'Privacy Statement' : 'Terms & Conditions';
|
||||
const title = contentType === 'dpa' ? 'Data Privacy Agreement' : 'Terms & Conditions';
|
||||
const contentUrl = `${this.apiPrefix}/${contentType}`;
|
||||
|
||||
// Use the composable to show modal and load content
|
||||
|
||||
@@ -104,12 +104,12 @@
|
||||
>
|
||||
<!-- Regular checkbox label -->
|
||||
<span v-if="!isConsentField" class="checkbox-text">{{ field.name }}</span>
|
||||
<!-- Consent field with privacy and terms links (rich, multilingual) -->
|
||||
<!-- Consent field with dpa and terms links (rich, multilingual) -->
|
||||
<ConsentRichText
|
||||
v-else
|
||||
class="checkbox-text consent-text"
|
||||
:template="texts.consentRich"
|
||||
:aria-privacy="texts.ariaPrivacy || 'Open privacy statement in a dialog'"
|
||||
:aria-privacy="texts.ariaPrivacy || 'Open dpa statement in a dialog'"
|
||||
:aria-terms="texts.ariaTerms || 'Open terms and conditions in a dialog'"
|
||||
@open-privacy="openPrivacyModal"
|
||||
@open-terms="openTermsModal"
|
||||
@@ -203,12 +203,12 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal', 'keydown-enter'],
|
||||
emits: ['update:modelValue', 'open-dpa-modal', 'open-terms-modal', 'keydown-enter'],
|
||||
setup() {
|
||||
// Consent text constants (English base) - rich template
|
||||
const consentTexts = {
|
||||
consentRich: "I agree with the <privacy>privacy statement</privacy> and the <terms>terms and conditions</terms>",
|
||||
ariaPrivacy: 'Open privacy statement in a dialog',
|
||||
consentRich: "I agree with the <dpa>dpa statement</dpa> and the <terms>terms and conditions</terms>",
|
||||
ariaPrivacy: 'Open dpa statement in a dialog',
|
||||
ariaTerms: 'Open terms and conditions in a dialog'
|
||||
};
|
||||
|
||||
@@ -259,8 +259,8 @@ export default {
|
||||
|
||||
// 4) Ultimate fallback (should not happen): provide a safe default
|
||||
return {
|
||||
consentRich: "I agree with the <privacy>privacy statement</privacy> and the <terms>terms and conditions</terms>",
|
||||
ariaPrivacy: 'Open privacy statement in a dialog',
|
||||
consentRich: "I agree with the <dpa>dpa statement</dpa> and the <terms>terms and conditions</terms>",
|
||||
ariaPrivacy: 'Open dpa statement in a dialog',
|
||||
ariaTerms: 'Open terms and conditions in a dialog'
|
||||
};
|
||||
},
|
||||
@@ -332,7 +332,7 @@ export default {
|
||||
}
|
||||
},
|
||||
openPrivacyModal() {
|
||||
this.$emit('open-privacy-modal');
|
||||
this.$emit('open-dpa-modal');
|
||||
},
|
||||
openTermsModal() {
|
||||
this.$emit('open-terms-modal');
|
||||
|
||||
@@ -387,35 +387,35 @@ def translate():
|
||||
@chat_bp.route('/privacy', methods=['GET'])
|
||||
def privacy_statement():
|
||||
"""
|
||||
Public AJAX endpoint for privacy statement content
|
||||
Public AJAX endpoint for dpa statement content
|
||||
Returns JSON response suitable for modal display
|
||||
"""
|
||||
try:
|
||||
# Use content_manager to get the latest privacy content
|
||||
content_data = content_manager.read_content('privacy')
|
||||
# Use content_manager to get the latest dpa content
|
||||
content_data = content_manager.read_content('dpa')
|
||||
|
||||
if not content_data:
|
||||
current_app.logger.error("Privacy statement content not found")
|
||||
current_app.logger.error("Data Privacy Agreement content not found")
|
||||
return jsonify({
|
||||
'error': 'Privacy statement not available',
|
||||
'message': 'The privacy statement could not be loaded at this time.'
|
||||
'error': 'Data Privacy Agreement not available',
|
||||
'message': 'The Data Pdpa Agreement could not be loaded at this time.'
|
||||
}), 404
|
||||
|
||||
current_app.logger.debug(f"Content data: {content_data}")
|
||||
|
||||
# Return JSON response for AJAX consumption
|
||||
return jsonify({
|
||||
'title': 'Privacy Statement',
|
||||
'title': 'Data Privacy Agreement',
|
||||
'content': content_data['content'],
|
||||
'version': content_data['version'],
|
||||
'content_type': content_data['content_type']
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error loading privacy statement: {str(e)}")
|
||||
current_app.logger.error(f"Error loading Data Privacy Agreement: {str(e)}")
|
||||
return jsonify({
|
||||
'error': 'Server error',
|
||||
'message': 'An error occurred while loading the privacy statement.'
|
||||
'message': 'An error occurred while loading the Data Privacy Agreement.'
|
||||
}), 500
|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ NO_CONTACT_DATA_QUESTIONS = [
|
||||
"Unfortunately, we can only move forward if you provide your contact details. Would you still consider sharing them with us?",
|
||||
"It’s totally your choice, of course. But without your contact details, we can’t proceed further. Would you be open to sharing them?",
|
||||
"We’d love to keep going, but we can only do so if we have your contact details. Would you like to provide them now?",
|
||||
"Your privacy matters, and we respect your decision. Just know that without your contact details, we’ll need to end the process here. Still interested in moving forward?",
|
||||
"Your dpa matters, and we respect your decision. Just know that without your contact details, we’ll need to end the process here. Still interested in moving forward?",
|
||||
"It’s a shame to stop here, but we do need your contact info to proceed. Would you like to share it so we can continue?"
|
||||
]
|
||||
CONTACT_DATA_PROCESSED_MESSAGE = "Thank you for allowing us to contact you."
|
||||
|
||||
@@ -147,7 +147,7 @@ NO_CONTACT_DATA_QUESTIONS = [
|
||||
"Unfortunately, we can only move forward if you provide your contact details. Would you still consider sharing them with us?",
|
||||
"It’s totally your choice, of course. But without your contact details, we can’t proceed further. Would you be open to sharing them?",
|
||||
"We’d love to keep going, but we can only do so if we have your contact details. Would you like to provide them now?",
|
||||
"Your privacy matters, and we respect your decision. Just know that without your contact details, we’ll need to end the process here. Still interested in moving forward?",
|
||||
"Your dpa matters, and we respect your decision. Just know that without your contact details, we’ll need to end the process here. Still interested in moving forward?",
|
||||
"It’s a shame to stop here, but we do need your contact info to proceed. Would you like to share it so we can continue?"
|
||||
]
|
||||
CONTACT_DATA_QUESTIONS = [
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Correct Not Null constraints on TenantConsent
|
||||
|
||||
Revision ID: 2228f7cafada
|
||||
Revises: a6ee51d72bb4
|
||||
Create Date: 2025-10-15 07:44:13.243434
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2228f7cafada'
|
||||
down_revision = 'a6ee51d72bb4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tenant_consent', schema=None) as batch_op:
|
||||
batch_op.alter_column('partner_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
batch_op.alter_column('partner_service_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
batch_op.drop_constraint('tenant_consent_created_by_fkey', type_='foreignkey')
|
||||
batch_op.drop_constraint('tenant_consent_user_id_fkey', type_='foreignkey')
|
||||
batch_op.drop_constraint('tenant_consent_partner_service_id_fkey', type_='foreignkey')
|
||||
batch_op.drop_constraint('tenant_consent_updated_by_fkey', type_='foreignkey')
|
||||
batch_op.drop_constraint('tenant_consent_partner_id_fkey', type_='foreignkey')
|
||||
batch_op.drop_constraint('tenant_consent_tenant_id_fkey', type_='foreignkey')
|
||||
batch_op.create_foreign_key(None, 'user', ['user_id'], ['id'], referent_schema='public')
|
||||
batch_op.create_foreign_key(None, 'user', ['updated_by'], ['id'], referent_schema='public')
|
||||
batch_op.create_foreign_key(None, 'partner_service', ['partner_service_id'], ['id'], referent_schema='public')
|
||||
batch_op.create_foreign_key(None, 'tenant', ['tenant_id'], ['id'], referent_schema='public')
|
||||
batch_op.create_foreign_key(None, 'partner', ['partner_id'], ['id'], referent_schema='public')
|
||||
batch_op.create_foreign_key(None, 'user', ['created_by'], ['id'], referent_schema='public')
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tenant_consent', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.create_foreign_key('tenant_consent_tenant_id_fkey', 'tenant', ['tenant_id'], ['id'])
|
||||
batch_op.create_foreign_key('tenant_consent_partner_id_fkey', 'partner', ['partner_id'], ['id'])
|
||||
batch_op.create_foreign_key('tenant_consent_updated_by_fkey', 'user', ['updated_by'], ['id'])
|
||||
batch_op.create_foreign_key('tenant_consent_partner_service_id_fkey', 'partner_service', ['partner_service_id'], ['id'])
|
||||
batch_op.create_foreign_key('tenant_consent_user_id_fkey', 'user', ['user_id'], ['id'])
|
||||
batch_op.create_foreign_key('tenant_consent_created_by_fkey', 'user', ['created_by'], ['id'])
|
||||
batch_op.alter_column('partner_service_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
batch_op.alter_column('partner_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,51 @@
|
||||
"""TenantConsent model creation
|
||||
|
||||
Revision ID: 411f5593460e
|
||||
Revises: 057fb975f0e3
|
||||
Create Date: 2025-10-09 07:32:04.598209
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '411f5593460e'
|
||||
down_revision = '057fb975f0e3'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('tenant_consent',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('partner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('partner_service_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('consent_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('consent_date', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('consent_dpa_version', sa.String(length=20), nullable=False),
|
||||
sa.Column('consent_t_c_version', sa.String(length=20), nullable=False),
|
||||
sa.Column('consent_data', sa.JSON(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||
sa.ForeignKeyConstraint(['partner_id'], ['public.partner.id'], ),
|
||||
sa.ForeignKeyConstraint(['partner_service_id'], ['public.partner_service.id'], ),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ),
|
||||
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['public.user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='public'
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('tenant_consent', schema='public')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Add ConsentVersion model
|
||||
|
||||
Revision ID: 8bfd440079a5
|
||||
Revises: 411f5593460e
|
||||
Create Date: 2025-10-09 14:12:41.318538
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8bfd440079a5'
|
||||
down_revision = '411f5593460e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('consent_version',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('consent_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('consent_version', sa.String(length=20), nullable=False),
|
||||
sa.Column('consent_valid_from', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('consent_valid_to', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='public'
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('consent_version', schema='public')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,36 @@
|
||||
"""consent_version iso sepearte version for t&c and dpa
|
||||
|
||||
Revision ID: a6ee51d72bb4
|
||||
Revises: f5f1a8b8e238
|
||||
Create Date: 2025-10-14 09:00:36.680468
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a6ee51d72bb4'
|
||||
down_revision = 'f5f1a8b8e238'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tenant_consent', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('consent_version', sa.String(length=20), nullable=False))
|
||||
batch_op.drop_column('consent_dpa_version')
|
||||
batch_op.drop_column('consent_t_c_version')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('tenant_consent', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('consent_t_c_version', sa.VARCHAR(length=20), autoincrement=False, nullable=False))
|
||||
batch_op.add_column(sa.Column('consent_dpa_version', sa.VARCHAR(length=20), autoincrement=False, nullable=False))
|
||||
batch_op.drop_column('consent_version')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Adding Tracking information to ConsentVersion
|
||||
|
||||
Revision ID: f5f1a8b8e238
|
||||
Revises: 8bfd440079a5
|
||||
Create Date: 2025-10-09 15:30:00.046174
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f5f1a8b8e238'
|
||||
down_revision = '8bfd440079a5'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('consent_version', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False))
|
||||
batch_op.add_column(sa.Column('created_by', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False))
|
||||
batch_op.add_column(sa.Column('updated_by', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('consent_version', schema=None) as batch_op:
|
||||
batch_op.drop_column('updated_by')
|
||||
batch_op.drop_column('updated_at')
|
||||
batch_op.drop_column('created_by')
|
||||
batch_op.drop_column('created_at')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -21,6 +21,7 @@ import '../../../eveai_app/static/assets/css/eveai-markdown.css'
|
||||
import '../../../eveai_app/static/assets/css/eveai-select2.css'
|
||||
import '../../../eveai_app/static/assets/css/eveai-tabulator.css'
|
||||
import '../../../eveai_app/static/assets/css/eveai-responsive-table.css'
|
||||
import '../../../eveai_app/static/assets/css/eveai-consent-viewer.css'
|
||||
|
||||
// Javascript Libraries
|
||||
|
||||
@@ -105,5 +106,8 @@ import '../../../eveai_app/static/assets/js/material-kit-pro.js';
|
||||
// });
|
||||
// }
|
||||
|
||||
// Eveai Consent Viewer
|
||||
import '../../../eveai_app/static/assets/js/eveai-consent-viewer.js'
|
||||
|
||||
// Eventueel een log om te bevestigen dat de bundel is geladen
|
||||
console.log('JavaScript en CSS bibliotheken gebundeld en geladen via main.js.');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
DEV_DB_HOST="localhost"
|
||||
DEV_DB_HOST="db"
|
||||
DEV_DB_PORT="5432"
|
||||
DEV_DB_NAME="eveai"
|
||||
DEV_DB_USER="luke"
|
||||
|
||||
Reference in New Issue
Block a user