"Refactor database connections and add analytics/permissions utils"
This commit is contained in:
parent
c8de6b6ca2
commit
b916cdaca3
Binary file not shown.
Binary file not shown.
@ -1 +1,752 @@
|
|||||||
|
/**
|
||||||
|
* MYP Platform Advanced UI Components
|
||||||
|
* Erweiterte Komponenten: Progress-Bars, File-Upload, Datepicker
|
||||||
|
* Version: 2.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Namespace erweitern
|
||||||
|
window.MYP = window.MYP || {};
|
||||||
|
window.MYP.Advanced = window.MYP.Advanced || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress Bar Component
|
||||||
|
*/
|
||||||
|
class ProgressBar {
|
||||||
|
constructor(container, options = {}) {
|
||||||
|
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
||||||
|
this.options = {
|
||||||
|
value: 0,
|
||||||
|
max: 100,
|
||||||
|
showLabel: true,
|
||||||
|
showPercentage: true,
|
||||||
|
animated: true,
|
||||||
|
color: 'blue',
|
||||||
|
size: 'md',
|
||||||
|
striped: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.currentValue = this.options.value;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!this.container) {
|
||||||
|
console.error('ProgressBar: Container nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const percentage = Math.round((this.currentValue / this.options.max) * 100);
|
||||||
|
const sizeClass = this.getSizeClass();
|
||||||
|
const colorClass = this.getColorClass();
|
||||||
|
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="progress-bar-container ${sizeClass}">
|
||||||
|
${this.options.showLabel ? `
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
${this.options.label || 'Fortschritt'}
|
||||||
|
</span>
|
||||||
|
${this.options.showPercentage ? `
|
||||||
|
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
${percentage}%
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="progress-bar-track ${sizeClass}">
|
||||||
|
<div class="progress-bar-fill ${colorClass} ${this.options.animated ? 'animated' : ''} ${this.options.striped ? 'striped' : ''}"
|
||||||
|
style="width: ${percentage}%"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow="${this.currentValue}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="${this.options.max}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSizeClass() {
|
||||||
|
const sizes = {
|
||||||
|
'sm': 'h-2',
|
||||||
|
'md': 'h-3',
|
||||||
|
'lg': 'h-4',
|
||||||
|
'xl': 'h-6'
|
||||||
|
};
|
||||||
|
return sizes[this.options.size] || sizes.md;
|
||||||
|
}
|
||||||
|
|
||||||
|
getColorClass() {
|
||||||
|
const colors = {
|
||||||
|
'blue': 'bg-blue-500',
|
||||||
|
'green': 'bg-green-500',
|
||||||
|
'red': 'bg-red-500',
|
||||||
|
'yellow': 'bg-yellow-500',
|
||||||
|
'purple': 'bg-purple-500',
|
||||||
|
'indigo': 'bg-indigo-500'
|
||||||
|
};
|
||||||
|
return colors[this.options.color] || colors.blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value, animate = true) {
|
||||||
|
const oldValue = this.currentValue;
|
||||||
|
this.currentValue = Math.max(0, Math.min(this.options.max, value));
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
this.animateToValue(oldValue, this.currentValue);
|
||||||
|
} else {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animateToValue(from, to) {
|
||||||
|
const duration = 500; // ms
|
||||||
|
const steps = 60;
|
||||||
|
const stepValue = (to - from) / steps;
|
||||||
|
let currentStep = 0;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
if (currentStep < steps) {
|
||||||
|
this.currentValue = from + (stepValue * currentStep);
|
||||||
|
this.render();
|
||||||
|
currentStep++;
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
this.currentValue = to;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
increment(amount = 1) {
|
||||||
|
this.setValue(this.currentValue + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement(amount = 1) {
|
||||||
|
this.setValue(this.currentValue - amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.setValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
this.setValue(this.options.max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced File Upload Component
|
||||||
|
*/
|
||||||
|
class FileUpload {
|
||||||
|
constructor(container, options = {}) {
|
||||||
|
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
||||||
|
this.options = {
|
||||||
|
multiple: false,
|
||||||
|
accept: '*/*',
|
||||||
|
maxSize: 50 * 1024 * 1024, // 50MB
|
||||||
|
maxFiles: 10,
|
||||||
|
dragDrop: true,
|
||||||
|
showProgress: true,
|
||||||
|
showPreview: true,
|
||||||
|
uploadUrl: '/api/upload',
|
||||||
|
chunkSize: 1024 * 1024, // 1MB chunks
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.files = [];
|
||||||
|
this.uploads = new Map();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!this.container) {
|
||||||
|
console.error('FileUpload: Container nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="file-upload-area" id="fileUploadArea">
|
||||||
|
<div class="file-upload-dropzone ${this.options.dragDrop ? 'drag-enabled' : ''}" id="dropzone">
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-slate-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||||
|
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="fileInput" class="cursor-pointer">
|
||||||
|
<span class="mt-2 block text-sm font-medium text-slate-900 dark:text-white">
|
||||||
|
${this.options.dragDrop ? 'Dateien hierher ziehen oder' : ''}
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 hover:text-blue-500">durchsuchen</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input type="file"
|
||||||
|
id="fileInput"
|
||||||
|
name="files"
|
||||||
|
${this.options.multiple ? 'multiple' : ''}
|
||||||
|
accept="${this.options.accept}"
|
||||||
|
class="sr-only">
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
${this.getFileTypeText()} • Max. ${this.formatFileSize(this.options.maxSize)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-list mt-4" id="fileList"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
const fileInput = this.container.querySelector('#fileInput');
|
||||||
|
const dropzone = this.container.querySelector('#dropzone');
|
||||||
|
|
||||||
|
// File Input Change
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
this.handleFiles(Array.from(e.target.files));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.options.dragDrop) {
|
||||||
|
// Drag and Drop Events
|
||||||
|
dropzone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropzone.classList.add('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropzone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropzone.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropzone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropzone.classList.remove('drag-over');
|
||||||
|
this.handleFiles(Array.from(e.dataTransfer.files));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFiles(fileList) {
|
||||||
|
for (const file of fileList) {
|
||||||
|
if (this.validateFile(file)) {
|
||||||
|
this.addFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFile(file) {
|
||||||
|
// Dateigröße prüfen
|
||||||
|
if (file.size > this.options.maxSize) {
|
||||||
|
this.showError(`Datei "${file.name}" ist zu groß. Maximum: ${this.formatFileSize(this.options.maxSize)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anzahl Dateien prüfen
|
||||||
|
if (!this.options.multiple && this.files.length > 0) {
|
||||||
|
this.files = []; // Ersetze einzelne Datei
|
||||||
|
} else if (this.files.length >= this.options.maxFiles) {
|
||||||
|
this.showError(`Maximal ${this.options.maxFiles} Dateien erlaubt`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(file) {
|
||||||
|
const fileData = {
|
||||||
|
id: this.generateId(),
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
status: 'pending',
|
||||||
|
progress: 0,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.files.push(fileData);
|
||||||
|
|
||||||
|
// Preview generieren
|
||||||
|
if (this.options.showPreview && file.type.startsWith('image/')) {
|
||||||
|
this.generatePreview(fileData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePreview(fileData) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
fileData.preview = e.target.result;
|
||||||
|
this.renderFileList();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(fileData.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFileList() {
|
||||||
|
const fileListContainer = this.container.querySelector('#fileList');
|
||||||
|
|
||||||
|
if (this.files.length === 0) {
|
||||||
|
fileListContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileListContainer.innerHTML = this.files.map(fileData => `
|
||||||
|
<div class="file-item" data-file-id="${fileData.id}">
|
||||||
|
<div class="flex items-center space-x-4 p-4 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
||||||
|
${fileData.preview ? `
|
||||||
|
<img src="${fileData.preview}" class="w-12 h-12 object-cover rounded" alt="Preview">
|
||||||
|
` : `
|
||||||
|
<div class="w-12 h-12 bg-slate-200 dark:bg-slate-700 rounded flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-slate-400" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-sm font-medium text-slate-900 dark:text-white truncate">
|
||||||
|
${fileData.name}
|
||||||
|
</p>
|
||||||
|
<button class="remove-file text-slate-400 hover:text-red-500" data-file-id="${fileData.id}">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
${this.formatFileSize(fileData.size)} • ${this.getStatusText(fileData.status)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
${this.options.showProgress && fileData.status === 'uploading' ? `
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="w-full bg-slate-200 dark:bg-slate-600 rounded-full h-2">
|
||||||
|
<div class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||||
|
style="width: ${fileData.progress}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${fileData.error ? `
|
||||||
|
<p class="text-xs text-red-500 mt-1">${fileData.error}</p>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Event Listeners für Remove-Buttons
|
||||||
|
fileListContainer.querySelectorAll('.remove-file').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const fileId = e.target.closest('.remove-file').dataset.fileId;
|
||||||
|
this.removeFile(fileId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFile(fileId) {
|
||||||
|
this.files = this.files.filter(f => f.id !== fileId);
|
||||||
|
this.renderFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFiles() {
|
||||||
|
const pendingFiles = this.files.filter(f => f.status === 'pending');
|
||||||
|
|
||||||
|
for (const fileData of pendingFiles) {
|
||||||
|
await this.uploadFile(fileData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(fileData) {
|
||||||
|
fileData.status = 'uploading';
|
||||||
|
fileData.progress = 0;
|
||||||
|
this.renderFileList();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileData.file);
|
||||||
|
|
||||||
|
const response = await fetch(this.options.uploadUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
fileData.status = 'completed';
|
||||||
|
fileData.progress = 100;
|
||||||
|
const result = await response.json();
|
||||||
|
fileData.url = result.url;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Upload fehlgeschlagen: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
fileData.status = 'error';
|
||||||
|
fileData.error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderFileList();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileTypeText() {
|
||||||
|
if (this.options.accept === '*/*') return 'Alle Dateitypen';
|
||||||
|
if (this.options.accept.includes('image/')) return 'Bilder';
|
||||||
|
if (this.options.accept.includes('.pdf')) return 'PDF-Dateien';
|
||||||
|
return 'Spezifische Dateitypen';
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatusText(status) {
|
||||||
|
const statusTexts = {
|
||||||
|
'pending': 'Wartend',
|
||||||
|
'uploading': 'Wird hochgeladen...',
|
||||||
|
'completed': 'Abgeschlossen',
|
||||||
|
'error': 'Fehler'
|
||||||
|
};
|
||||||
|
return statusTexts[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
generateId() {
|
||||||
|
return 'file_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
if (window.showToast) {
|
||||||
|
window.showToast(message, 'error');
|
||||||
|
} else {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFiles() {
|
||||||
|
return this.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompletedFiles() {
|
||||||
|
return this.files.filter(f => f.status === 'completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.files = [];
|
||||||
|
this.renderFileList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Datepicker Component
|
||||||
|
*/
|
||||||
|
class DatePicker {
|
||||||
|
constructor(input, options = {}) {
|
||||||
|
this.input = typeof input === 'string' ? document.querySelector(input) : input;
|
||||||
|
this.options = {
|
||||||
|
format: 'dd.mm.yyyy',
|
||||||
|
minDate: null,
|
||||||
|
maxDate: null,
|
||||||
|
disabledDates: [],
|
||||||
|
language: 'de',
|
||||||
|
closeOnSelect: true,
|
||||||
|
showWeekNumbers: false,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.isOpen = false;
|
||||||
|
this.currentDate = new Date();
|
||||||
|
this.selectedDate = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!this.input) {
|
||||||
|
console.error('DatePicker: Input-Element nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupInput();
|
||||||
|
this.createCalendar();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInput() {
|
||||||
|
this.input.setAttribute('readonly', 'true');
|
||||||
|
this.input.classList.add('datepicker-input');
|
||||||
|
|
||||||
|
// Container für Input und Calendar
|
||||||
|
this.container = document.createElement('div');
|
||||||
|
this.container.className = 'datepicker-container relative';
|
||||||
|
this.input.parentNode.insertBefore(this.container, this.input);
|
||||||
|
this.container.appendChild(this.input);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCalendar() {
|
||||||
|
this.calendar = document.createElement('div');
|
||||||
|
this.calendar.className = 'datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden';
|
||||||
|
this.calendar.innerHTML = this.renderCalendar();
|
||||||
|
this.container.appendChild(this.calendar);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCalendar() {
|
||||||
|
const year = this.currentDate.getFullYear();
|
||||||
|
const month = this.currentDate.getMonth();
|
||||||
|
const monthName = this.getMonthName(month);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="datepicker-header p-4 border-b border-slate-200 dark:border-slate-600">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button type="button" class="prev-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="text-sm font-medium text-slate-900 dark:text-white">
|
||||||
|
${monthName} ${year}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="next-month p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datepicker-body p-4">
|
||||||
|
<div class="grid grid-cols-7 gap-1 mb-2">
|
||||||
|
${this.getWeekdayHeaders()}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-1">
|
||||||
|
${this.getDaysOfMonth(year, month)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWeekdayHeaders() {
|
||||||
|
const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||||
|
return weekdays.map(day =>
|
||||||
|
`<div class="text-xs font-medium text-slate-500 dark:text-slate-400 text-center p-1">${day}</div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
getDaysOfMonth(year, month) {
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const startDate = new Date(firstDay);
|
||||||
|
startDate.setDate(startDate.getDate() - ((firstDay.getDay() + 6) % 7));
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
const current = new Date(startDate);
|
||||||
|
|
||||||
|
while (current <= lastDay || current.getMonth() === month) {
|
||||||
|
const isCurrentMonth = current.getMonth() === month;
|
||||||
|
const isToday = this.isToday(current);
|
||||||
|
const isSelected = this.isSelectedDate(current);
|
||||||
|
const isDisabled = this.isDisabledDate(current);
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors',
|
||||||
|
isCurrentMonth ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-600',
|
||||||
|
isToday ? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100' : '',
|
||||||
|
isSelected ? 'bg-blue-500 text-white' : '',
|
||||||
|
!isDisabled && isCurrentMonth ? 'hover:bg-slate-100 dark:hover:bg-slate-700' : '',
|
||||||
|
isDisabled ? 'cursor-not-allowed opacity-50' : ''
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
days.push(`
|
||||||
|
<div class="${classes.join(' ')}"
|
||||||
|
data-date="${this.formatDateForData(current)}"
|
||||||
|
${isDisabled ? '' : 'data-selectable="true"'}>
|
||||||
|
${current.getDate()}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
|
||||||
|
if (days.length >= 42) break; // Max 6 Wochen
|
||||||
|
}
|
||||||
|
|
||||||
|
return days.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Input click
|
||||||
|
this.input.addEventListener('click', () => {
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calendar clicks
|
||||||
|
this.calendar.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('prev-month')) {
|
||||||
|
this.previousMonth();
|
||||||
|
} else if (e.target.classList.contains('next-month')) {
|
||||||
|
this.nextMonth();
|
||||||
|
} else if (e.target.dataset.selectable) {
|
||||||
|
this.selectDate(new Date(e.target.dataset.date));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!this.container.contains(e.target)) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.calendar.classList.remove('hidden');
|
||||||
|
this.isOpen = true;
|
||||||
|
this.updateCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.calendar.classList.add('hidden');
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectDate(date) {
|
||||||
|
this.selectedDate = new Date(date);
|
||||||
|
this.input.value = this.formatDate(date);
|
||||||
|
|
||||||
|
// Custom Event
|
||||||
|
this.input.dispatchEvent(new CustomEvent('dateselected', {
|
||||||
|
detail: { date: new Date(date) }
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.options.closeOnSelect) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousMonth() {
|
||||||
|
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
|
||||||
|
this.updateCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextMonth() {
|
||||||
|
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
|
||||||
|
this.updateCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCalendar() {
|
||||||
|
this.calendar.innerHTML = this.renderCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
isToday(date) {
|
||||||
|
const today = new Date();
|
||||||
|
return date.toDateString() === today.toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectedDate(date) {
|
||||||
|
return this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisabledDate(date) {
|
||||||
|
if (this.options.minDate && date < this.options.minDate) return true;
|
||||||
|
if (this.options.maxDate && date > this.options.maxDate) return true;
|
||||||
|
return this.options.disabledDates.some(disabled =>
|
||||||
|
date.toDateString() === disabled.toDateString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
|
||||||
|
return this.options.format
|
||||||
|
.replace('dd', day)
|
||||||
|
.replace('mm', month)
|
||||||
|
.replace('yyyy', year);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDateForData(date) {
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getMonthName(monthIndex) {
|
||||||
|
const months = [
|
||||||
|
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
||||||
|
];
|
||||||
|
return months[monthIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(date) {
|
||||||
|
if (date) {
|
||||||
|
this.selectDate(new Date(date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue() {
|
||||||
|
return this.selectedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.selectedDate = null;
|
||||||
|
this.input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globale API
|
||||||
|
window.MYP.Advanced = {
|
||||||
|
ProgressBar,
|
||||||
|
FileUpload,
|
||||||
|
DatePicker,
|
||||||
|
|
||||||
|
// Convenience Functions
|
||||||
|
createProgressBar: (container, options) => new ProgressBar(container, options),
|
||||||
|
createFileUpload: (container, options) => new FileUpload(container, options),
|
||||||
|
createDatePicker: (input, options) => new DatePicker(input, options)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Auto-initialize datepickers
|
||||||
|
document.querySelectorAll('[data-datepicker]').forEach(input => {
|
||||||
|
const options = JSON.parse(input.dataset.datepicker || '{}');
|
||||||
|
new DatePicker(input, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-initialize file uploads
|
||||||
|
document.querySelectorAll('[data-file-upload]').forEach(container => {
|
||||||
|
const options = JSON.parse(container.dataset.fileUpload || '{}');
|
||||||
|
new FileUpload(container, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🚀 MYP Advanced Components geladen');
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
@ -263,68 +263,66 @@
|
|||||||
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
|
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<!-- Benachrichtigungen -->
|
<!-- Benachrichtigungen - kompakteres Design -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
id="notificationToggle"
|
id="notificationToggle"
|
||||||
class="relative p-2 rounded-full text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50 transition-all duration-200"
|
class="relative p-1.5 rounded-full text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50 transition-all duration-200"
|
||||||
aria-label="Benachrichtigungen anzeigen"
|
aria-label="Benachrichtigungen anzeigen"
|
||||||
title="Benachrichtigungen"
|
title="Benachrichtigungen"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Badge für ungelesene Benachrichtigungen -->
|
<!-- Badge für ungelesene Benachrichtigungen -->
|
||||||
<span id="notificationBadge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium hidden">
|
<span id="notificationBadge" class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-medium hidden">
|
||||||
0
|
0
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Benachrichtigungs-Dropdown -->
|
<!-- Benachrichtigungs-Dropdown -->
|
||||||
<div id="notificationDropdown" class="absolute right-0 mt-2 w-80 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 z-50 hidden">
|
<div id="notificationDropdown" class="absolute right-0 mt-2 w-72 sm:w-80 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 z-50 hidden">
|
||||||
<div class="p-4 border-b border-slate-200 dark:border-slate-600">
|
<div class="p-3 border-b border-slate-200 dark:border-slate-600">
|
||||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Benachrichtigungen</h3>
|
<h3 class="text-base font-semibold text-slate-900 dark:text-white">Benachrichtigungen</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="notificationList" class="max-h-96 overflow-y-auto">
|
<div id="notificationList" class="max-h-80 overflow-y-auto">
|
||||||
<div class="p-4 text-center text-slate-500 dark:text-slate-400">
|
<div class="p-3 text-center text-slate-500 dark:text-slate-400 text-sm">
|
||||||
Keine neuen Benachrichtigungen
|
Keine neuen Benachrichtigungen
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 border-t border-slate-200 dark:border-slate-600">
|
<div class="p-2 border-t border-slate-200 dark:border-slate-600">
|
||||||
<button id="markAllRead" class="w-full text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors">
|
<button id="markAllRead" class="w-full text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors">
|
||||||
Alle als gelesen markieren
|
Alle als gelesen markieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Profile Dropdown - neues Design -->
|
<!-- User Profile Dropdown - kompakteres Design -->
|
||||||
<div class="relative" id="user-menu-container">
|
<div class="relative" id="user-menu-container">
|
||||||
<button
|
<button
|
||||||
id="user-menu-button"
|
id="user-menu-button"
|
||||||
class="user-menu-button-new"
|
class="flex items-center space-x-1 rounded-full p-1 text-slate-700 dark:text-slate-300 hover:bg-slate-100/80 dark:hover:bg-slate-800/50 transition-all duration-200"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-label="Benutzermenu öffnen"
|
aria-label="Benutzermenu öffnen"
|
||||||
>
|
>
|
||||||
<!-- Profile Avatar -->
|
<!-- Profile Avatar -->
|
||||||
<div class="user-avatar-new">
|
<div class="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-medium">
|
||||||
{{ current_user.email[0].upper() if current_user.email else 'U' }}
|
{{ current_user.email[0].upper() if current_user.email else 'U' }}
|
||||||
</div>
|
</div>
|
||||||
<!-- User Info (hidden on mobile) -->
|
<!-- User Info (nur auf größeren Geräten) -->
|
||||||
<div class="hidden md:block text-left ml-2">
|
<div class="hidden sm:block text-left ml-1">
|
||||||
<div class="text-sm font-semibold text-slate-900 dark:text-white transition-colors duration-300">{{ current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}</div>
|
<div class="text-xs font-medium text-slate-900 dark:text-white transition-colors duration-300">{{ current_user.email.split('@')[0] if current_user.email else 'Benutzer' }}</div>
|
||||||
<div class="text-xs text-slate-600 dark:text-slate-400 transition-colors duration-300">Mercedes-Benz Mitarbeiter</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Login Button - neues Design -->
|
<!-- Login Button - kompakteres Design -->
|
||||||
<a href="{{ url_for('login') }}"
|
<a href="{{ url_for('login') }}"
|
||||||
class="login-button-new">
|
class="flex items-center space-x-1 py-1 px-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs transition-colors duration-200">
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden sm:inline">Anmelden</span>
|
<span class="hidden sm:inline">Anmelden</span>
|
||||||
|
@ -321,7 +321,7 @@ window.isAdmin = {% if current_user.is_admin %}true{% else %}false{% endif %};
|
|||||||
|
|
||||||
// Live Time Update
|
// Live Time Update
|
||||||
function updateLiveTime() {
|
function updateLiveTime() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timeElement = document.getElementById('live-time');
|
const timeElement = document.getElementById('live-time');
|
||||||
if (timeElement) {
|
if (timeElement) {
|
||||||
timeElement.textContent = now.toLocaleTimeString('de-DE');
|
timeElement.textContent = now.toLocaleTimeString('de-DE');
|
||||||
@ -360,8 +360,5 @@ function closeJobModal() {
|
|||||||
function closeExtendModal() {
|
function closeExtendModal() {
|
||||||
hideModal('extendJobModal');
|
hideModal('extendJobModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... existing code ...
|
|
||||||
</script>
|
</script>
|
||||||
// ... existing code ...
|
|
||||||
{% endblock %}
|
{% endblock %}
|
1
backend/app/utils/analytics.py
Normal file
1
backend/app/utils/analytics.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
630
backend/app/utils/permissions.py
Normal file
630
backend/app/utils/permissions.py
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Erweiterte Berechtigungsverwaltung für MYP Platform
|
||||||
|
Granulare Rollen und Permissions für feingranulare Zugriffskontrolle
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from functools import wraps
|
||||||
|
from typing import List, Dict, Set, Optional
|
||||||
|
from flask import current_user, request, jsonify, abort
|
||||||
|
from flask_login import login_required
|
||||||
|
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Table, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("permissions")
|
||||||
|
|
||||||
|
# ===== PERMISSION DEFINITIONS =====
|
||||||
|
|
||||||
|
class Permission(Enum):
|
||||||
|
"""Alle verfügbaren Berechtigungen im System"""
|
||||||
|
|
||||||
|
# Basis-Berechtigungen
|
||||||
|
LOGIN = "login"
|
||||||
|
VIEW_DASHBOARD = "view_dashboard"
|
||||||
|
|
||||||
|
# Drucker-Berechtigungen
|
||||||
|
VIEW_PRINTERS = "view_printers"
|
||||||
|
CREATE_PRINTER = "create_printer"
|
||||||
|
EDIT_PRINTER = "edit_printer"
|
||||||
|
DELETE_PRINTER = "delete_printer"
|
||||||
|
CONTROL_PRINTER = "control_printer" # Ein-/Ausschalten
|
||||||
|
VIEW_PRINTER_DETAILS = "view_printer_details"
|
||||||
|
|
||||||
|
# Job-Berechtigungen
|
||||||
|
VIEW_JOBS = "view_jobs"
|
||||||
|
CREATE_JOB = "create_job"
|
||||||
|
EDIT_OWN_JOB = "edit_own_job"
|
||||||
|
EDIT_ALL_JOBS = "edit_all_jobs"
|
||||||
|
DELETE_OWN_JOB = "delete_own_job"
|
||||||
|
DELETE_ALL_JOBS = "delete_all_jobs"
|
||||||
|
EXTEND_JOB = "extend_job"
|
||||||
|
CANCEL_JOB = "cancel_job"
|
||||||
|
VIEW_JOB_HISTORY = "view_job_history"
|
||||||
|
|
||||||
|
# Benutzer-Berechtigungen
|
||||||
|
VIEW_USERS = "view_users"
|
||||||
|
CREATE_USER = "create_user"
|
||||||
|
EDIT_USER = "edit_user"
|
||||||
|
DELETE_USER = "delete_user"
|
||||||
|
MANAGE_ROLES = "manage_roles"
|
||||||
|
VIEW_USER_DETAILS = "view_user_details"
|
||||||
|
|
||||||
|
# Admin-Berechtigungen
|
||||||
|
VIEW_ADMIN_PANEL = "view_admin_panel"
|
||||||
|
MANAGE_SYSTEM = "manage_system"
|
||||||
|
VIEW_LOGS = "view_logs"
|
||||||
|
EXPORT_DATA = "export_data"
|
||||||
|
BACKUP_DATABASE = "backup_database"
|
||||||
|
MANAGE_SETTINGS = "manage_settings"
|
||||||
|
|
||||||
|
# Gast-Berechtigungen
|
||||||
|
VIEW_GUEST_REQUESTS = "view_guest_requests"
|
||||||
|
CREATE_GUEST_REQUEST = "create_guest_request"
|
||||||
|
APPROVE_GUEST_REQUEST = "approve_guest_request"
|
||||||
|
DENY_GUEST_REQUEST = "deny_guest_request"
|
||||||
|
MANAGE_GUEST_REQUESTS = "manage_guest_requests"
|
||||||
|
|
||||||
|
# Statistik-Berechtigungen
|
||||||
|
VIEW_STATS = "view_stats"
|
||||||
|
VIEW_DETAILED_STATS = "view_detailed_stats"
|
||||||
|
EXPORT_STATS = "export_stats"
|
||||||
|
|
||||||
|
# Kalender-Berechtigungen
|
||||||
|
VIEW_CALENDAR = "view_calendar"
|
||||||
|
EDIT_CALENDAR = "edit_calendar"
|
||||||
|
MANAGE_SHIFTS = "manage_shifts"
|
||||||
|
|
||||||
|
# Wartung-Berechtigungen
|
||||||
|
SCHEDULE_MAINTENANCE = "schedule_maintenance"
|
||||||
|
VIEW_MAINTENANCE = "view_maintenance"
|
||||||
|
PERFORM_MAINTENANCE = "perform_maintenance"
|
||||||
|
|
||||||
|
class Role(Enum):
|
||||||
|
"""Vordefinierte Rollen mit Standard-Berechtigungen"""
|
||||||
|
|
||||||
|
GUEST = "guest"
|
||||||
|
USER = "user"
|
||||||
|
POWER_USER = "power_user"
|
||||||
|
TECHNICIAN = "technician"
|
||||||
|
SUPERVISOR = "supervisor"
|
||||||
|
ADMIN = "admin"
|
||||||
|
SUPER_ADMIN = "super_admin"
|
||||||
|
|
||||||
|
# ===== ROLE PERMISSIONS MAPPING =====
|
||||||
|
|
||||||
|
ROLE_PERMISSIONS = {
|
||||||
|
Role.GUEST: {
|
||||||
|
Permission.LOGIN,
|
||||||
|
Permission.VIEW_PRINTERS,
|
||||||
|
Permission.CREATE_GUEST_REQUEST,
|
||||||
|
Permission.VIEW_CALENDAR,
|
||||||
|
},
|
||||||
|
|
||||||
|
Role.USER: {
|
||||||
|
Permission.LOGIN,
|
||||||
|
Permission.VIEW_DASHBOARD,
|
||||||
|
Permission.VIEW_PRINTERS,
|
||||||
|
Permission.VIEW_JOBS,
|
||||||
|
Permission.CREATE_JOB,
|
||||||
|
Permission.EDIT_OWN_JOB,
|
||||||
|
Permission.DELETE_OWN_JOB,
|
||||||
|
Permission.EXTEND_JOB,
|
||||||
|
Permission.CANCEL_JOB,
|
||||||
|
Permission.VIEW_STATS,
|
||||||
|
Permission.VIEW_CALENDAR,
|
||||||
|
Permission.CREATE_GUEST_REQUEST,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Power User erweitert User-Permissions
|
||||||
|
ROLE_PERMISSIONS[Role.POWER_USER] = ROLE_PERMISSIONS[Role.USER] | {
|
||||||
|
Permission.VIEW_PRINTER_DETAILS,
|
||||||
|
Permission.VIEW_JOB_HISTORY,
|
||||||
|
Permission.VIEW_DETAILED_STATS,
|
||||||
|
Permission.EXPORT_STATS,
|
||||||
|
Permission.VIEW_GUEST_REQUESTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Technician erweitert Power User-Permissions
|
||||||
|
ROLE_PERMISSIONS[Role.TECHNICIAN] = ROLE_PERMISSIONS[Role.POWER_USER] | {
|
||||||
|
Permission.CONTROL_PRINTER,
|
||||||
|
Permission.EDIT_PRINTER,
|
||||||
|
Permission.SCHEDULE_MAINTENANCE,
|
||||||
|
Permission.VIEW_MAINTENANCE,
|
||||||
|
Permission.PERFORM_MAINTENANCE,
|
||||||
|
Permission.EDIT_CALENDAR,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Supervisor erweitert Technician-Permissions
|
||||||
|
ROLE_PERMISSIONS[Role.SUPERVISOR] = ROLE_PERMISSIONS[Role.TECHNICIAN] | {
|
||||||
|
Permission.CREATE_PRINTER,
|
||||||
|
Permission.EDIT_ALL_JOBS,
|
||||||
|
Permission.DELETE_ALL_JOBS,
|
||||||
|
Permission.VIEW_USERS,
|
||||||
|
Permission.APPROVE_GUEST_REQUEST,
|
||||||
|
Permission.DENY_GUEST_REQUEST,
|
||||||
|
Permission.MANAGE_GUEST_REQUESTS,
|
||||||
|
Permission.MANAGE_SHIFTS,
|
||||||
|
Permission.VIEW_USER_DETAILS,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Admin erweitert Supervisor-Permissions
|
||||||
|
ROLE_PERMISSIONS[Role.ADMIN] = ROLE_PERMISSIONS[Role.SUPERVISOR] | {
|
||||||
|
Permission.DELETE_PRINTER,
|
||||||
|
Permission.VIEW_ADMIN_PANEL,
|
||||||
|
Permission.CREATE_USER,
|
||||||
|
Permission.EDIT_USER,
|
||||||
|
Permission.DELETE_USER,
|
||||||
|
Permission.EXPORT_DATA,
|
||||||
|
Permission.VIEW_LOGS,
|
||||||
|
Permission.MANAGE_SETTINGS,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Super Admin hat alle Berechtigungen
|
||||||
|
ROLE_PERMISSIONS[Role.SUPER_ADMIN] = {perm for perm in Permission}
|
||||||
|
|
||||||
|
# ===== DATABASE MODELS EXTENSIONS =====
|
||||||
|
|
||||||
|
# Many-to-Many Tabelle für User-Permissions
|
||||||
|
user_permissions = Table('user_permissions',
|
||||||
|
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
|
||||||
|
Column('permission_id', Integer, ForeignKey('permissions.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Many-to-Many Tabelle für User-Roles
|
||||||
|
user_roles = Table('user_roles',
|
||||||
|
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
|
||||||
|
Column('role_id', Integer, ForeignKey('roles.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
class PermissionModel:
|
||||||
|
"""Datenbank-Model für Berechtigungen"""
|
||||||
|
|
||||||
|
__tablename__ = 'permissions'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(100), unique=True, nullable=False)
|
||||||
|
description = Column(String(255))
|
||||||
|
category = Column(String(50)) # Gruppierung von Berechtigungen
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
class RoleModel:
|
||||||
|
"""Datenbank-Model für Rollen"""
|
||||||
|
|
||||||
|
__tablename__ = 'roles'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(50), unique=True, nullable=False)
|
||||||
|
display_name = Column(String(100))
|
||||||
|
description = Column(String(255))
|
||||||
|
is_system_role = Column(Boolean, default=False) # System-Rollen können nicht gelöscht werden
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
permissions = relationship("PermissionModel", secondary="role_permissions", back_populates="roles")
|
||||||
|
|
||||||
|
class UserPermissionOverride:
|
||||||
|
"""Temporäre oder spezielle Berechtigungsüberschreibungen"""
|
||||||
|
|
||||||
|
__tablename__ = 'user_permission_overrides'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
|
permission = Column(String(100), nullable=False)
|
||||||
|
granted = Column(Boolean, nullable=False) # True = gewährt, False = verweigert
|
||||||
|
reason = Column(String(255))
|
||||||
|
granted_by = Column(Integer, ForeignKey('users.id'))
|
||||||
|
expires_at = Column(DateTime, nullable=True) # NULL = permanent
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
# ===== PERMISSION CHECKER CLASS =====
|
||||||
|
|
||||||
|
class PermissionChecker:
|
||||||
|
"""Hauptklasse für Berechtigungsprüfungen"""
|
||||||
|
|
||||||
|
def __init__(self, user=None):
|
||||||
|
self.user = user or current_user
|
||||||
|
self._permission_cache = {}
|
||||||
|
self._cache_timeout = timedelta(minutes=5)
|
||||||
|
self._cache_timestamp = None
|
||||||
|
|
||||||
|
def has_permission(self, permission: Permission) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft ob der Benutzer eine bestimmte Berechtigung hat
|
||||||
|
|
||||||
|
Args:
|
||||||
|
permission: Die zu prüfende Berechtigung
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Berechtigung vorhanden
|
||||||
|
"""
|
||||||
|
if not self.user or not self.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cache prüfen
|
||||||
|
if self._is_cache_valid() and permission.value in self._permission_cache:
|
||||||
|
return self._permission_cache[permission.value]
|
||||||
|
|
||||||
|
# Berechtigungen neu berechnen
|
||||||
|
has_perm = self._calculate_permission(permission)
|
||||||
|
|
||||||
|
# Cache aktualisieren
|
||||||
|
self._update_cache(permission.value, has_perm)
|
||||||
|
|
||||||
|
return has_perm
|
||||||
|
|
||||||
|
def _calculate_permission(self, permission: Permission) -> bool:
|
||||||
|
"""Berechnet ob eine Berechtigung vorhanden ist"""
|
||||||
|
|
||||||
|
# Super Admin hat alle Rechte
|
||||||
|
if hasattr(self.user, 'is_super_admin') and self.user.is_super_admin:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Explizite Überschreibungen prüfen
|
||||||
|
override = self._check_permission_override(permission)
|
||||||
|
if override is not None:
|
||||||
|
return override
|
||||||
|
|
||||||
|
# Rollen-basierte Berechtigungen prüfen
|
||||||
|
user_roles = self._get_user_roles()
|
||||||
|
for role in user_roles:
|
||||||
|
if permission in ROLE_PERMISSIONS.get(role, set()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Direkte Benutzer-Berechtigungen prüfen
|
||||||
|
if hasattr(self.user, 'permissions'):
|
||||||
|
user_permissions = [Permission(p.name) for p in self.user.permissions if hasattr(Permission, p.name.upper())]
|
||||||
|
if permission in user_permissions:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_permission_override(self, permission: Permission) -> Optional[bool]:
|
||||||
|
"""Prüft ob es eine Berechtigungsüberschreibung gibt"""
|
||||||
|
if not hasattr(self.user, 'permission_overrides'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
for override in self.user.permission_overrides:
|
||||||
|
if (override.permission == permission.value and
|
||||||
|
(override.expires_at is None or override.expires_at > now)):
|
||||||
|
logger.info(f"Permission override angewendet: {permission.value} = {override.granted} für User {self.user.id}")
|
||||||
|
return override.granted
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_user_roles(self) -> List[Role]:
|
||||||
|
"""Holt die Rollen des Benutzers"""
|
||||||
|
roles = []
|
||||||
|
|
||||||
|
# Legacy Admin-Check
|
||||||
|
if hasattr(self.user, 'is_admin') and self.user.is_admin:
|
||||||
|
roles.append(Role.ADMIN)
|
||||||
|
|
||||||
|
# Neue Rollen-System
|
||||||
|
if hasattr(self.user, 'roles'):
|
||||||
|
for role_model in self.user.roles:
|
||||||
|
try:
|
||||||
|
role = Role(role_model.name)
|
||||||
|
roles.append(role)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Unbekannte Rolle: {role_model.name}")
|
||||||
|
|
||||||
|
# Standard-Rolle wenn keine andere definiert
|
||||||
|
if not roles:
|
||||||
|
roles.append(Role.USER)
|
||||||
|
|
||||||
|
return roles
|
||||||
|
|
||||||
|
def _is_cache_valid(self) -> bool:
|
||||||
|
"""Prüft ob der Permission-Cache noch gültig ist"""
|
||||||
|
if self._cache_timestamp is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return datetime.now() - self._cache_timestamp < self._cache_timeout
|
||||||
|
|
||||||
|
def _update_cache(self, permission: str, has_permission: bool):
|
||||||
|
"""Aktualisiert den Permission-Cache"""
|
||||||
|
if self._cache_timestamp is None or not self._is_cache_valid():
|
||||||
|
self._permission_cache = {}
|
||||||
|
self._cache_timestamp = datetime.now()
|
||||||
|
|
||||||
|
self._permission_cache[permission] = has_permission
|
||||||
|
|
||||||
|
def get_all_permissions(self) -> Set[Permission]:
|
||||||
|
"""Gibt alle Berechtigungen des Benutzers zurück"""
|
||||||
|
permissions = set()
|
||||||
|
|
||||||
|
for permission in Permission:
|
||||||
|
if self.has_permission(permission):
|
||||||
|
permissions.add(permission)
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
def can_access_resource(self, resource_type: str, resource_id: int = None, action: str = "view") -> bool:
|
||||||
|
"""
|
||||||
|
Prüft Zugriff auf spezifische Ressourcen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_type: Art der Ressource (job, printer, user, etc.)
|
||||||
|
resource_id: ID der Ressource (optional)
|
||||||
|
action: Aktion (view, edit, delete, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Zugriff erlaubt
|
||||||
|
"""
|
||||||
|
# Resource-spezifische Logik
|
||||||
|
if resource_type == "job":
|
||||||
|
return self._check_job_access(resource_id, action)
|
||||||
|
elif resource_type == "printer":
|
||||||
|
return self._check_printer_access(resource_id, action)
|
||||||
|
elif resource_type == "user":
|
||||||
|
return self._check_user_access(resource_id, action)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_job_access(self, job_id: int, action: str) -> bool:
|
||||||
|
"""Prüft Job-spezifische Zugriffsrechte"""
|
||||||
|
if action == "view":
|
||||||
|
if self.has_permission(Permission.VIEW_JOBS):
|
||||||
|
return True
|
||||||
|
elif action == "edit":
|
||||||
|
if self.has_permission(Permission.EDIT_ALL_JOBS):
|
||||||
|
return True
|
||||||
|
if self.has_permission(Permission.EDIT_OWN_JOB) and job_id:
|
||||||
|
# Prüfen ob eigener Job (vereinfacht)
|
||||||
|
return self._is_own_job(job_id)
|
||||||
|
elif action == "delete":
|
||||||
|
if self.has_permission(Permission.DELETE_ALL_JOBS):
|
||||||
|
return True
|
||||||
|
if self.has_permission(Permission.DELETE_OWN_JOB) and job_id:
|
||||||
|
return self._is_own_job(job_id)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_printer_access(self, printer_id: int, action: str) -> bool:
|
||||||
|
"""Prüft Drucker-spezifische Zugriffsrechte"""
|
||||||
|
if action == "view":
|
||||||
|
return self.has_permission(Permission.VIEW_PRINTERS)
|
||||||
|
elif action == "edit":
|
||||||
|
return self.has_permission(Permission.EDIT_PRINTER)
|
||||||
|
elif action == "delete":
|
||||||
|
return self.has_permission(Permission.DELETE_PRINTER)
|
||||||
|
elif action == "control":
|
||||||
|
return self.has_permission(Permission.CONTROL_PRINTER)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_user_access(self, user_id: int, action: str) -> bool:
|
||||||
|
"""Prüft Benutzer-spezifische Zugriffsrechte"""
|
||||||
|
if action == "view":
|
||||||
|
if self.has_permission(Permission.VIEW_USERS):
|
||||||
|
return True
|
||||||
|
# Eigenes Profil ansehen
|
||||||
|
if user_id == self.user.id:
|
||||||
|
return True
|
||||||
|
elif action == "edit":
|
||||||
|
if self.has_permission(Permission.EDIT_USER):
|
||||||
|
return True
|
||||||
|
# Eigenes Profil bearbeiten (begrenzt)
|
||||||
|
if user_id == self.user.id:
|
||||||
|
return True
|
||||||
|
elif action == "delete":
|
||||||
|
if self.has_permission(Permission.DELETE_USER) and user_id != self.user.id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _is_own_job(self, job_id: int) -> bool:
|
||||||
|
"""Hilfsfunktion um zu prüfen ob Job dem Benutzer gehört"""
|
||||||
|
# Vereinfachte Implementierung - sollte mit DB-Query implementiert werden
|
||||||
|
try:
|
||||||
|
from models import Job, get_db_session
|
||||||
|
db_session = get_db_session()
|
||||||
|
job = db_session.query(Job).filter(Job.id == job_id).first()
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
return job and (job.user_id == self.user.id or job.owner_id == self.user.id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Job-Ownership-Check: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ===== DECORATORS =====
|
||||||
|
|
||||||
|
def require_permission(permission: Permission):
|
||||||
|
"""
|
||||||
|
Decorator der eine bestimmte Berechtigung erfordert
|
||||||
|
|
||||||
|
Args:
|
||||||
|
permission: Die erforderliche Berechtigung
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
@login_required
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
checker = PermissionChecker()
|
||||||
|
|
||||||
|
if not checker.has_permission(permission):
|
||||||
|
logger.warning(f"Zugriff verweigert: User {current_user.id} hat keine Berechtigung {permission.value}")
|
||||||
|
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Insufficient permissions',
|
||||||
|
'message': f'Berechtigung "{permission.value}" erforderlich',
|
||||||
|
'required_permission': permission.value
|
||||||
|
}), 403
|
||||||
|
else:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_role(role: Role):
|
||||||
|
"""
|
||||||
|
Decorator der eine bestimmte Rolle erfordert
|
||||||
|
|
||||||
|
Args:
|
||||||
|
role: Die erforderliche Rolle
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
@login_required
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
checker = PermissionChecker()
|
||||||
|
user_roles = checker._get_user_roles()
|
||||||
|
|
||||||
|
if role not in user_roles:
|
||||||
|
logger.warning(f"Zugriff verweigert: User {current_user.id} hat nicht die Rolle {role.value}")
|
||||||
|
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Insufficient role',
|
||||||
|
'message': f'Rolle "{role.value}" erforderlich',
|
||||||
|
'required_role': role.value
|
||||||
|
}), 403
|
||||||
|
else:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_resource_access(resource_type: str, action: str = "view"):
|
||||||
|
"""
|
||||||
|
Decorator für ressourcen-spezifische Berechtigungsprüfung
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_type: Art der Ressource
|
||||||
|
action: Erforderliche Aktion
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
@login_required
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Resource ID aus URL-Parametern extrahieren
|
||||||
|
resource_id = kwargs.get('id') or kwargs.get(f'{resource_type}_id')
|
||||||
|
|
||||||
|
checker = PermissionChecker()
|
||||||
|
|
||||||
|
if not checker.can_access_resource(resource_type, resource_id, action):
|
||||||
|
logger.warning(f"Ressourcen-Zugriff verweigert: User {current_user.id}, {resource_type}:{resource_id}, Action: {action}")
|
||||||
|
|
||||||
|
if request.path.startswith('/api/'):
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Resource access denied',
|
||||||
|
'message': f'Zugriff auf {resource_type} nicht erlaubt',
|
||||||
|
'resource_type': resource_type,
|
||||||
|
'action': action
|
||||||
|
}), 403
|
||||||
|
else:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
# ===== UTILITY FUNCTIONS =====
|
||||||
|
|
||||||
|
def check_permission(permission: Permission, user=None) -> bool:
|
||||||
|
"""
|
||||||
|
Standalone-Funktion zur Berechtigungsprüfung
|
||||||
|
|
||||||
|
Args:
|
||||||
|
permission: Die zu prüfende Berechtigung
|
||||||
|
user: Benutzer (optional, default: current_user)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Berechtigung vorhanden
|
||||||
|
"""
|
||||||
|
checker = PermissionChecker(user)
|
||||||
|
return checker.has_permission(permission)
|
||||||
|
|
||||||
|
def get_user_permissions(user=None) -> Set[Permission]:
|
||||||
|
"""
|
||||||
|
Gibt alle Berechtigungen eines Benutzers zurück
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: Benutzer (optional, default: current_user)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set[Permission]: Alle Berechtigungen des Benutzers
|
||||||
|
"""
|
||||||
|
checker = PermissionChecker(user)
|
||||||
|
return checker.get_all_permissions()
|
||||||
|
|
||||||
|
def grant_temporary_permission(user_id: int, permission: Permission, duration_hours: int = 24, reason: str = "", granted_by_id: int = None):
|
||||||
|
"""
|
||||||
|
Gewährt temporäre Berechtigung
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID des Benutzers
|
||||||
|
permission: Die zu gewährende Berechtigung
|
||||||
|
duration_hours: Dauer in Stunden
|
||||||
|
reason: Begründung
|
||||||
|
granted_by_id: ID des gewährenden Benutzers
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from models import get_db_session
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
override = UserPermissionOverride(
|
||||||
|
user_id=user_id,
|
||||||
|
permission=permission.value,
|
||||||
|
granted=True,
|
||||||
|
reason=reason,
|
||||||
|
granted_by=granted_by_id or (current_user.id if current_user.is_authenticated else None),
|
||||||
|
expires_at=datetime.now() + timedelta(hours=duration_hours)
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.add(override)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
logger.info(f"Temporäre Berechtigung gewährt: {permission.value} für User {user_id} ({duration_hours}h)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Gewähren temporärer Berechtigung: {e}")
|
||||||
|
|
||||||
|
# ===== TEMPLATE HELPERS =====
|
||||||
|
|
||||||
|
def init_permission_helpers(app):
|
||||||
|
"""
|
||||||
|
Registriert Template-Helper für Berechtigungen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: Flask-App-Instanz
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.template_global()
|
||||||
|
def has_permission(permission_name: str) -> bool:
|
||||||
|
"""Template Helper für Berechtigungsprüfung"""
|
||||||
|
try:
|
||||||
|
permission = Permission(permission_name)
|
||||||
|
return check_permission(permission)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@app.template_global()
|
||||||
|
def has_role(role_name: str) -> bool:
|
||||||
|
"""Template Helper für Rollenprüfung"""
|
||||||
|
try:
|
||||||
|
role = Role(role_name)
|
||||||
|
checker = PermissionChecker()
|
||||||
|
return role in checker._get_user_roles()
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@app.template_global()
|
||||||
|
def can_access(resource_type: str, resource_id: int = None, action: str = "view") -> bool:
|
||||||
|
"""Template Helper für Ressourcen-Zugriff"""
|
||||||
|
checker = PermissionChecker()
|
||||||
|
return checker.can_access_resource(resource_type, resource_id, action)
|
||||||
|
|
||||||
|
logger.info("🔐 Permission Template Helpers registriert")
|
Loading…
x
Reference in New Issue
Block a user