first commit

This commit is contained in:
ghostersk
2025-05-25 20:26:18 +01:00
commit 5375ef6121
77 changed files with 9073 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>{{ company.name }} - Add User</h2>
<div class="card">
<div class="card-body">
{% if users %}
<form method="POST" action="{{ url_for('auth.add_company_user', company_id=company.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="user_id" class="form-label">Select User</label>
<select class="form-select" id="user_id" name="user_id" required>
<option value="" selected disabled>Choose a user</option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.username }} ({{ user.email }})</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<select class="form-select" id="role" name="role" required>
<option value="User">User</option>
{% if current_user.is_global_admin() %}
<option value="CompanyAdmin">Company Admin</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<a href="{{ url_for('auth.company_users', company_id=company.id) }}" class="btn btn-secondary me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Add User</button>
</div>
</form>
{% else %}
<div class="alert alert-info">
<p>All users are already added to this company.</p>
<a href="{{ url_for('auth.manage_users') }}" class="btn btn-primary mt-2">Create New User</a>
</div>
<div class="mt-3">
<a href="{{ url_for('auth.company_users', company_id=company.id) }}" class="btn btn-secondary">Back to Company Users</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+161
View File
@@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Admin Settings</h2>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Website Settings</h5>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="allow_registration"
name="allow_registration" {% if settings.allow_registration %}checked{% endif %}>
<label class="form-check-label" for="allow_registration">
Allow User Registration
</label>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="restrict_email_domains"
name="restrict_email_domains" {% if settings.restrict_email_domains %}checked{% endif %}>
<label class="form-check-label" for="restrict_email_domains">
Restrict Registration to Specific Email Domains
</label>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="require_mfa_for_all_users"
name="require_mfa_for_all_users" {% if settings.require_mfa_for_all_users %}checked{% endif %}>
<label class="form-check-label" for="require_mfa_for_all_users">
Require MFA for All Users (GlobalAdmin accounts exempt)
</label>
</div>
<button type="submit" class="btn btn-primary">Save Settings</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Password Strength Requirements</h5>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="password_min_length" class="form-label">Minimum Password Length</label>
<input type="number" class="form-control" id="password_min_length"
name="password_min_length" min="6" max="128"
value="{{ settings.password_min_length or 10 }}" required>
<div class="form-text">Minimum 6 characters required</div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="password_require_numbers_mixed_case"
name="password_require_numbers_mixed_case"
{% if settings.password_require_numbers_mixed_case %}checked{% endif %}>
<label class="form-check-label" for="password_require_numbers_mixed_case">
Require Numbers and Mixed Case Letters
</label>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="password_require_special_chars"
name="password_require_special_chars"
{% if settings.password_require_special_chars %}checked{% endif %}>
<label class="form-check-label" for="password_require_special_chars">
Require Special Characters
</label>
</div>
<div class="mb-3">
<label for="password_safe_special_chars" class="form-label">Safe Special Characters</label>
<input type="text" class="form-control" id="password_safe_special_chars"
name="password_safe_special_chars"
value="{{ settings.password_safe_special_chars or '!@#$%^&*()_+-=[]{}|;:,.<>?' }}">
<div class="form-text">Characters allowed for special character requirement</div>
</div>
<button type="submit" class="btn btn-primary">Save Password Settings</button>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Database Logging Configuration</h5>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="log_level" class="form-label">Database Logging Level</label>
<select class="form-select" id="log_level" name="log_level">
{% for level_value, level_description in available_log_levels %}
<option value="{{ level_value }}"
{% if settings.log_level == level_value %}selected{% endif %}>
{{ level_description }}
</option>
{% endfor %}
</select>
<div class="form-text">
Controls which log messages are saved to the database. Lower levels include all higher levels.
<br><strong>Note:</strong> DEBUG and INFO levels may generate many log entries and increase database size.
</div>
</div>
<button type="submit" class="btn btn-primary">Save Logging Settings</button>
</form>
<div class="mt-3">
<h6>Current Log Level: <span class="badge bg-primary">{{ settings.log_level or 'WARNING' }}</span></h6>
<div class="small text-muted">
<p><strong>Level Descriptions:</strong></p>
<ul class="mb-0">
<li><strong>DEBUG:</strong> All messages including detailed debugging information</li>
<li><strong>INFO:</strong> General information (logins, registrations, etc.)</li>
<li><strong>WARNING:</strong> Warnings and potential issues (failed logins, etc.)</li>
<li><strong>ERROR:</strong> Error messages and exceptions</li>
<li><strong>CRITICAL:</strong> Only critical system errors</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Allowed Email Domains</h5>
<form method="POST" action="{{ url_for('auth.add_allowed_domain') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group mb-3">
<input type="text" class="form-control" name="domain"
placeholder="example.com" required>
<button class="btn btn-outline-primary" type="submit">Add Domain</button>
</div>
</form>
<div class="mt-3">
{% if allowed_domains %}
<ul class="list-group">
{% for domain in allowed_domains %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ domain.domain }}
<form method="POST" action="{{ url_for('auth.delete_allowed_domain', domain_id=domain.id) }}"
style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('Are you sure you want to remove this domain?')">
Remove
</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">No domains added yet. When domains are restricted, only users with these email domains can register.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+111
View File
@@ -0,0 +1,111 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>{{ company.name }} - Sites (API Keys)</h2>
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">Create New API Key</h5>
<a href="{{ url_for('auth.download_agent', company_id=company.id) }}" class="btn btn-success">
<i class="fas fa-download"></i> Download Windows Agent
</a>
</div>
<form method="POST" action="{{ url_for('auth.create_company_api_key', company_id=company.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<input type="text" class="form-control" id="description" name="description" required>
</div>
<button type="submit" class="btn btn-primary">Generate New Key</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Company Sites (API Keys)</h5>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> These API keys can be used to authenticate windows agents. You can
<a href="{{ url_for('auth.download_agent', company_id=company.id) }}">download a pre-configured agent</a>
with your selected API key.
</div>
<div class="table-responsive">
<table class="table table-striped" id="apiKeysTable">
<thead>
<tr>
<th>Description</th>
<th>Key</th>
<th>Created</th>
<th>Last Used</th>
<th>Created By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for api_key in api_keys %}
<tr>
<td>{{ api_key.description }}</td>
<td>
<div class="input-group">
<input type="text" class="form-control" value="{{ api_key.key }}" readonly>
<button class="btn btn-outline-secondary copy-btn" type="button" data-key="{{ api_key.key }}">
Copy
</button>
</div>
</td>
<td>{{ api_key.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ api_key.last_used.strftime('%Y-%m-%d %H:%M:%S') if api_key.last_used else 'Never' }}</td>
<td>{{ api_key.user.username }}</td>
<td>
<form action="{{ url_for('auth.delete_company_api_key', company_id=company.id, key_id=api_key.id) }}" method="POST"
style="display:inline" onsubmit="return confirm('Are you sure you want to delete this API key?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<a href="{{ url_for('auth.manage_companies') }}" class="btn btn-secondary">Back to Companies</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<script>
$(document).ready(function() {
$('#apiKeysTable').DataTable({
"pageLength": 10,
"order": [[2, "desc"]]
});
document.querySelectorAll('.copy-btn').forEach(button => {
button.addEventListener('click', function() {
const key = this.dataset.key;
navigator.clipboard.writeText(key).then(() => {
this.textContent = 'Copied!';
setTimeout(() => this.textContent = 'Copy', 2000);
});
});
});
});
</script>
{% endblock %}
+126
View File
@@ -0,0 +1,126 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>{{ company.name }} - User Management</h2>
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">Company Users</h5>
<div>
<a href="{{ url_for('auth.download_agent', company_id=company.id) }}" class="btn btn-success me-2">
<i class="fas fa-download"></i> Download Agent
</a>
<a href="{{ url_for('auth.create_company_user', company_id=company.id) }}" class="btn btn-success me-2">Create New User</a>
<a href="{{ url_for('auth.add_company_user', company_id=company.id) }}" class="btn btn-primary">Add Existing User</a>
</div>
</div>
<div class="table-responsive mt-3">
<table class="table table-striped" id="companyUsersTable">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for uc in user_companies %}
<tr>
<td>{{ uc.user.username }}</td>
<td>{{ uc.user.email }}</td>
<td>
{% if current_user.is_global_admin() or current_user.is_company_admin(company.id) %}
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="roleDropdown{{ uc.id }}" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false">
{{ uc.role }}
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="roleDropdown{{ uc.id }}">
<li>
<form action="{{ url_for('auth.change_company_user_role', company_id=company.id, user_id=uc.user_id) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="role" value="User">
<button type="submit" class="dropdown-item {% if uc.role == 'User' %}active{% endif %}">User</button>
</form>
</li>
<li>
<form action="{{ url_for('auth.change_company_user_role', company_id=company.id, user_id=uc.user_id) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="role" value="CompanyAdmin">
<button type="submit" class="dropdown-item {% if uc.role == 'CompanyAdmin' %}active{% endif %}">Company Admin</button>
</form>
</li>
</ul>
</div>
{% else %}
{{ uc.role }}
{% endif %}
</td>
<td>
<form action="{{ url_for('auth.remove_company_user', company_id=company.id, user_id=uc.user_id) }}" method="POST" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-danger"
{% if uc.role == 'CompanyAdmin' and not current_user.is_global_admin() %}disabled{% endif %}
onclick="return confirm('Are you sure you want to remove this user from the company?')">
Remove
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<a href="{{ url_for('auth.manage_companies') }}" class="btn btn-secondary">Back to Companies</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
$('#companyUsersTable').DataTable({
"pageLength": 10,
"order": [[0, "asc"]]
});
// Fix dropdown position for items near the bottom of the screen
$('.dropdown-toggle').on('click', function() {
var $button = $(this);
var $dropdownMenu = $button.next('.dropdown-menu');
// Get positions
var buttonOffset = $button.offset();
var buttonHeight = $button.outerHeight();
var dropdownHeight = $dropdownMenu.outerHeight();
// Calculate if dropdown would go off screen
var bottomSpace = $(window).height() - (buttonOffset.top - $(window).scrollTop() + buttonHeight);
if (bottomSpace < dropdownHeight) {
// Not enough space below, make it open upwards
$(this).parent().addClass('dropup');
} else {
$(this).parent().removeClass('dropup');
}
});
});
</script>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Create New Company</h2>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('auth.create_company') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="name" class="form-label">Company Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<a href="{{ url_for('auth.manage_companies') }}" class="btn btn-secondary me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create Company</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>{{ company.name }} - Create New User</h2>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('auth.create_company_user', company_id=company.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<a href="{{ url_for('auth.company_users', company_id=company.id) }}" class="btn btn-secondary me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+89
View File
@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% block title %}Download Agent - {{ company.name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Download Agent for {{ company.name }}</h5>
</div>
<div class="card-body">
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle"></i>
Configure and download the Windows monitoring agent with your company's API key pre-configured.
</div>
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Agent Configuration</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.download_agent', company_id=company.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="api_key" class="form-label">Select API Key</label>
<select class="form-select" id="api_key" name="api_key" required>
<option value="">Select API Key</option>
{% for api_key in api_keys %}
<option value="{{ api_key.id }}">
{{ api_key.key[:8] }}...{{ api_key.key[-8:] }}
{% if api_key.description %}({{ api_key.description }}){% endif %}
</option>
{% endfor %}
</select>
{% if not api_keys %}
<div class="form-text text-warning">
No API keys found. <a href="{{ url_for('auth.company_api_keys', company_id=company.id) }}">Create an API key</a> first.
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="server_url" class="form-label">Server URL</label>
<input type="url" class="form-control" id="server_url" name="server_url" value="{{ default_url }}" required>
<div class="form-text">
The URL where the agent will send login events. Usually the same URL as this website.
</div>
</div>
<div class="mb-3">
<label for="install_dir" class="form-label">Installation Directory (Optional)</label>
<input type="text" class="form-control" id="install_dir" name="install_dir" placeholder="C:\ProgramData\UserSessionMon">
<div class="form-text">
Custom installation directory for the agent. Leave empty to use the default path.
</div>
</div>
<button type="submit" class="btn btn-primary" {% if not api_keys %}disabled{% endif %}>
<i class="fas fa-download"></i> Download Agent
</button>
</form>
</div>
</div>
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Installation Instructions</h5>
</div>
<div class="card-body">
<ol>
<li>Download the agent package using the form above.</li>
<li>Extract the ZIP file to a folder on your Windows computer.</li>
<li>Run the agent as administrator to install it:
<code>winagentUSM.exe --service install</code>
</li>
<li>The service will start automatically and begin monitoring login events.</li>
<li>Events will be sent to this server using the specified API key.</li>
</ol>
<p><strong>Note:</strong> The agent requires administrator privileges to install and run as a Windows service.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2>Edit Company</h2>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('auth.edit_company', company_id=company.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="name" class="form-label">Company Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ company.name }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ company.description }}</textarea>
</div>
<div class="mb-3">
<a href="{{ url_for('auth.manage_companies') }}" class="btn btn-secondary me-2">Cancel</a>
<button type="submit" class="btn btn-primary">Update Company</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+405
View File
@@ -0,0 +1,405 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.dataTables.min.css') }}" rel="stylesheet">
<style>
.log-level-CRITICAL { color: #dc3545; font-weight: bold; }
.log-level-ERROR { color: #dc3545; }
.log-level-WARNING { color: #ffc107; }
.log-level-INFO { color: #0dcaf0; }
.log-level-DEBUG { color: #6c757d; }
.log-message {
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.exception-toggle {
cursor: pointer;
color: #0d6efd;
text-decoration: underline;
}
.exception-details {
background-color: #343a40;
border: 1px solid #495057;
border-radius: 0.375rem;
padding: 1rem;
margin-top: 0.5rem;
font-family: monospace;
font-size: 0.875rem;
white-space: pre-wrap;
overflow-x: auto;
}
.filter-form {
background-color: #343a40;
border-radius: 0.375rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.stats-cards {
margin-bottom: 1.5rem;
}
.stat-card {
background-color: #343a40;
border: 1px solid #495057;
border-radius: 0.375rem;
padding: 1rem;
text-align: center;
cursor: pointer;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stat-label {
color: #adb5bd;
font-size: 0.875rem;
}
</style>
{% endblock %}
{% block title %}Error Logs{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Application Error Logs</h2>
<div>
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#clearLogsModal">
Clear Old Logs
</button>
<button id="refresh-logs" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-refresh"></i> Refresh
</button>
</div>
</div>
<!-- Statistics Cards -->
<div class="row stats-cards">
<div class="col-md">
<div class="stat-card">
<div class="stat-number text-info" id="debug-count">{{ error_logs | selectattr('level', 'equalto', 'DEBUG') | list | length }}</div>
<div class="stat-label">Debug</div>
</div>
</div>
<div class="col-md">
<div class="stat-card">
<div class="stat-number text-info" id="info-count">{{ error_logs | selectattr('level', 'equalto', 'INFO') | list | length }}</div>
<div class="stat-label">Info</div>
</div>
</div>
<div class="col-md">
<div class="stat-card">
<div class="stat-number text-danger" id="critical-count">{{ error_logs | selectattr('level', 'equalto', 'CRITICAL') | list | length }}</div>
<div class="stat-label">Critical</div>
</div>
</div>
<div class="col-md">
<div class="stat-card">
<div class="stat-number text-danger" id="error-count">{{ error_logs | selectattr('level', 'equalto', 'ERROR') | list | length }}</div>
<div class="stat-label">Errors</div>
</div>
</div>
<div class="col-md">
<div class="stat-card">
<div class="stat-number text-warning" id="warning-count">{{ error_logs | selectattr('level', 'equalto', 'WARNING') | list | length }}</div>
<div class="stat-label">Warnings</div>
</div>
</div>
<div class="col-md">
<div class="stat-card">
<div class="stat-number text-info" id="total-count">{{ error_logs | length }}</div>
<div class="stat-label">Total Logs</div>
</div>
</div>
</div>
<!-- Filter Form -->
<div class="filter-form">
<form method="GET" action="{{ url_for('auth.view_error_logs') }}">
<div class="row align-items-end">
<div class="col-md-3">
<label for="start_date" class="form-label">Start Date:</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="{{ start_date }}">
</div>
<div class="col-md-3">
<label for="end_date" class="form-label">End Date:</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ end_date }}">
</div>
<div class="col-md-3">
<label for="level" class="form-label">Log Level:</label>
<select class="form-select" id="level" name="level">
<option value="">All Levels</option>
{% for level in available_levels %}
<option value="{{ level }}" {% if level == level_filter %}selected{% endif %}>{{ level }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary">Apply Filters</button>
<a href="{{ url_for('auth.view_error_logs') }}" class="btn btn-outline-secondary">Reset</a>
</div>
</div>
</form>
</div>
<!-- Error Logs Table -->
<div class="card">
<div class="card-body">
<table id="errorLogsTable" class="table table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>Timestamp</th>
<th>Level</th>
<th>Logger</th>
<th>Message</th>
<th>User</th>
<th>IP Address</th>
<th>Request ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for log in error_logs %}
<tr>
<td data-order="{{ log.timestamp.strftime('%Y%m%d%H%M%S') }}">
{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}
</td>
<td>
<span class="log-level-{{ log.level }}">{{ log.level }}</span>
</td>
<td>{{ log.logger_name or 'N/A' }}</td>
<td>
<div class="log-message" title="{{ log.message }}">{{ log.message }}</div>
{% if log.exception %}
<small class="exception-toggle" onclick="toggleException({{ log.id }})">
View Exception
</small>
<div id="exception-{{ log.id }}" class="exception-details" style="display: none;">
{{ log.exception }}
</div>
{% endif %}
</td>
<td>{{ log.user_id or 'N/A' }}</td>
<td>{{ log.remote_addr or 'N/A' }}</td>
<td>{{ log.request_id or 'N/A' }}</td>
<td>
<button class="btn btn-sm btn-outline-info" onclick="viewLogDetail({{ log.id }})">
Details
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Clear Logs Modal -->
<div class="modal fade" id="clearLogsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Clear Old Error Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>This will permanently delete error logs older than the specified number of days.</p>
<div class="mb-3">
<label for="daysToKeep" class="form-label">Keep logs for (days):</label>
<input type="number" class="form-control" id="daysToKeep" value="30" min="1" max="365">
<div class="form-text">Logs older than this many days will be deleted.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" onclick="clearOldLogs()">Clear Logs</button>
</div>
</div>
</div>
</div>
<!-- Log Detail Modal -->
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Error Log Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="logDetailContent">
<!-- Content loaded via JavaScript -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.buttons.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.bootstrap5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.html5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/jszip.min.js') }}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
var table = $('#errorLogsTable').DataTable({
pageLength: 25,
lengthMenu: [[25, 50, 100, 200, -1], [25, 50, 100, 200, "All"]],
order: [[0, 'desc']], // Sort by timestamp descending
dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
buttons: [
{
extend: 'csv',
text: 'Export CSV',
className: 'btn btn-secondary btn-sm',
filename: 'error_logs_' + new Date().toISOString().split('T')[0]
}
],
language: {
search: "Search logs:",
lengthMenu: "Show _MENU_ logs per page",
info: "Showing _START_ to _END_ of _TOTAL_ logs",
paginate: {
first: "First",
last: "Last",
next: "Next",
previous: "Previous"
}
}
});
// Stats card click events
$('#debug-count').closest('.stat-card').on('click', function() {
table.column(1).search('Debug').draw();
});
$('#info-count').closest('.stat-card').on('click', function() {
table.column(1).search('Info').draw();
});
$('#critical-count').closest('.stat-card').on('click', function() {
table.column(1).search('CRITICAL').draw();
});
$('#error-count').closest('.stat-card').on('click', function() {
table.column(1).search('ERROR').draw();
});
$('#warning-count').closest('.stat-card').on('click', function() {
table.column(1).search('WARNING').draw();
});
$('#total-count').closest('.stat-card').on('click', function() {
table.column(1).search('').draw();
});
// Refresh button
$('#refresh-logs').on('click', function() {
location.reload();
});
});
function toggleException(logId) {
var element = document.getElementById('exception-' + logId);
if (element.style.display === 'none') {
element.style.display = 'block';
} else {
element.style.display = 'none';
}
}
function viewLogDetail(logId) {
fetch(`{{ url_for('auth.view_error_log_detail', log_id=0) }}`.replace('0', logId))
.then(response => response.json())
.then(data => {
var content = `
<div class="row">
<div class="col-md-6"><strong>ID:</strong> ${data.id}</div>
<div class="col-md-6"><strong>Level:</strong> <span class="log-level-${data.level}">${data.level}</span></div>
</div>
<div class="row mt-2">
<div class="col-md-6"><strong>Logger:</strong> ${data.logger_name || 'N/A'}</div>
<div class="col-md-6"><strong>Timestamp:</strong> ${new Date(data.timestamp).toLocaleString()}</div>
</div>
<div class="row mt-2">
<div class="col-md-6"><strong>User ID:</strong> ${data.user_id || 'N/A'}</div>
<div class="col-md-6"><strong>IP Address:</strong> ${data.remote_addr || 'N/A'}</div>
</div>
<div class="row mt-2">
<div class="col-md-6"><strong>Request ID:</strong> ${data.request_id || 'N/A'}</div>
<div class="col-md-6"><strong>File:</strong> ${data.pathname || 'N/A'}${data.lineno ? ':' + data.lineno : ''}</div>
</div>
<div class="mt-3">
<strong>Message:</strong>
<div class="exception-details mt-1">${data.message}</div>
</div>
${data.exception ? `
<div class="mt-3">
<strong>Exception:</strong>
<div class="exception-details mt-1">${data.exception}</div>
</div>
` : ''}
`;
document.getElementById('logDetailContent').innerHTML = content;
new bootstrap.Modal(document.getElementById('logDetailModal')).show();
})
.catch(error => {
alert('Error loading log details: ' + error);
});
}
function clearOldLogs() {
var daysToKeep = document.getElementById('daysToKeep').value;
if (!confirm(`Are you sure you want to delete all error logs older than ${daysToKeep} days? This action cannot be undone.`)) {
return;
}
fetch(`{{ url_for('auth.clear_error_logs') }}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': $('meta[name=csrf-token]').attr('content')
},
body: JSON.stringify({
days_to_keep: parseInt(daysToKeep)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
alert('Error clearing logs: ' + error);
});
// Close modal
bootstrap.Modal.getInstance(document.getElementById('clearLogsModal')).hide();
}
</script>
{% endblock %}
+133
View File
@@ -0,0 +1,133 @@
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3>Login</h3>
</div>
<div class="card-body">
{% if error_message %}
<div class="alert alert-danger">{{ error_message }}</div>
{% endif %}
<form id="loginForm" method="POST" action="">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="mb-3 form-check">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
</div>
{% if allow_registration %}
<div class="card-footer">
<small>Need an account? <a href="{{ url_for('auth.register') }}">Sign up now</a></small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Error Modal -->
<div class="modal fade" id="loginErrorModal" tabindex="-1" aria-labelledby="loginErrorModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginErrorModalLabel">Login Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="loginErrorModalBody">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% include 'auth/mfa_modal.html' %}
{% endblock %}
{% block scripts %}
<script>
// Get CSRF token from the rendered template
const csrfToken = '{{ csrf_token() }}';
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
fetch('{{ url_for("auth.login") }}', {
method: 'POST',
body: new FormData(this),
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
}
})
.then(response => response.json().then(data => ({status: response.status, body: data})))
.then(({status, body}) => {
if (body.require_mfa) {
const modal = new bootstrap.Modal(document.getElementById('mfaModal'));
modal.show();
} else if (body.require_mfa_setup) {
// Redirect to MFA setup for required users
window.location.href = '{{ url_for("auth.setup_mfa") }}';
} else if (body.redirect) {
window.location.href = body.redirect;
} else if (body.error) {
document.getElementById('loginErrorModalBody').textContent = body.error;
const errorModal = new bootstrap.Modal(document.getElementById('loginErrorModal'));
errorModal.show();
}
})
.catch(error => {
document.getElementById('loginErrorModalBody').textContent = 'An unexpected error occurred.';
const errorModal = new bootstrap.Modal(document.getElementById('loginErrorModal'));
errorModal.show();
});
});
document.getElementById('mfaForm').addEventListener('submit', function(e) {
e.preventDefault();
fetch('{{ url_for("auth.verify_mfa") }}', {
method: 'POST',
body: new FormData(this),
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.redirect) {
window.location.href = data.redirect;
} else {
alert('Invalid MFA code');
}
})
.catch(error => {
alert('Failed to verify MFA code');
});
});
</script>
{% endblock %}
+92
View File
@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Company Management</h2>
<div class="card mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">Companies</h5>
<a href="{{ url_for('auth.create_company') }}" class="btn btn-primary">Create New Company</a>
</div>
<div class="table-responsive mt-3">
<table class="table table-striped" id="companiesTable">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for company in companies %}
<tr>
<td>{{ company.name }}</td>
<td>{{ company.description }}</td>
<td>{{ company.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('auth.edit_company', company_id=company.id) }}" class="btn btn-sm btn-primary">Edit</a>
<a href="{{ url_for('auth.company_users', company_id=company.id) }}" class="btn btn-sm btn-info">Manage Users</a>
<a href="{{ url_for('auth.company_api_keys', company_id=company.id) }}" class="btn btn-sm btn-secondary">Sites (API Key)</a>
<button type="button" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#deleteCompanyModal{{ company.id }}">
Delete
</button>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteCompanyModal{{ company.id }}" tabindex="-1" aria-labelledby="deleteCompanyModalLabel{{ company.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteCompanyModalLabel{{ company.id }}">Confirm Deletion</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to delete the company: <strong>{{ company.name }}</strong>?
<p class="text-danger mt-2">This action cannot be undone and will remove this company from the system. The company's API keys will be deleted and all users will be removed from this company (but users will not be deleted from the system).</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form action="{{ url_for('auth.delete_company', company_id=company.id) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger">Delete Company</button>
</form>
</div>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<script>
$(document).ready(function() {
$('#companiesTable').DataTable({
"pageLength": 10,
"order": [[0, "asc"]]
});
});
</script>
{% endblock %}
+839
View File
@@ -0,0 +1,839 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
{% endblock %}
{% block title %}Manage Users{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>Manage Users</h2>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Create New User</h5>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row">
<div class="col-md-4 mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="col-md-4 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="col-md-4 mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="role" class="form-label">Role</label>
<select class="form-select" id="role" name="role" required>
<option value="User">User</option>
{% if current_user.role == 'GlobalAdmin' %}
<option value="Admin">Admin</option>
<option value="GlobalAdmin">Global Admin</option>
{% endif %}
</select>
</div>
<div class="col-md-4 mb-3">
<div class="form-check mt-4">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active">
<label class="form-check-label" for="is_active">Account Active</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Create User</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Existing Users</h5>
<div class="table-responsive">
<table class="table table-striped" id="usersTable">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Companies</th>
<th>Status</th>
<th>2FA</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
{% if current_user.role == 'GlobalAdmin' and user.id != current_user.id %}
<!-- Role dropdown for Global Admins -->
<div class="dropdown">
<button class="btn btn-sm dropdown-toggle
{% if user.role == 'GlobalAdmin' %}btn-danger
{% elif user.role == 'Admin' %}btn-warning
{% else %}btn-info{% endif %}"
type="button" data-bs-toggle="dropdown">
{{ user.role }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="changeUserRole({{ user.id }}, 'User')">User</a></li>
<li><a class="dropdown-item" href="#" onclick="changeUserRole({{ user.id }}, 'Admin')">Admin</a></li>
<li><a class="dropdown-item" href="#" onclick="changeUserRole({{ user.id }}, 'GlobalAdmin')">Global Admin</a></li>
</ul>
</div>
{% else %}
<!-- Static display for non-Global Admins or current user -->
{% if user.role == 'GlobalAdmin' %}
<span class="badge bg-danger">Global Admin</span>
{% elif user.role == 'Admin' %}
<span class="badge bg-warning">Admin</span>
{% else %}
<span class="badge bg-info">User</span>
{% endif %}
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#companiesModal{{ user.id }}">
{% if user.filtered_companies %}
{{ user.filtered_companies|length }} Companies
{% else %}
No Companies
{% endif %}
</button>
<!-- Company Management Modal -->
<div class="modal fade" id="companiesModal{{ user.id }}" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Manage Companies for {{ user.username }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- Current Companies -->
<h6>Current Companies:</h6>
{% if current_user.role == 'Admin' and user.companies|length > user.filtered_companies|length %}
<div class="alert alert-info" role="alert">
<small><i class="fas fa-info-circle"></i>
This user belongs to {{ user.companies|length }} companies total, but you can only see and manage {{ user.filtered_companies|length }} companies that you also have access to.</small>
</div>
{% endif %}
<div class="table-responsive mb-3">
<table class="table table-sm">
<thead>
<tr>
<th>Company</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="currentCompanies{{ user.id }}">
{% if user.filtered_companies %}
{% for uc in user.filtered_companies %}
<tr id="company-row-{{ user.id }}-{{ uc.company_id }}">
<td>{{ uc.company.name }}</td>
<td>
{% if current_user.role == 'GlobalAdmin' or (current_user.role == 'Admin' and user.role not in ['Admin', 'GlobalAdmin']) %}
<select class="form-select form-select-sm"
onchange="changeCompanyRole({{ user.id }}, {{ uc.company_id }}, this.value)">
<option value="User" {% if uc.role == 'User' %}selected{% endif %}>User</option>
{% if current_user.role == 'GlobalAdmin' %}
<option value="CompanyAdmin" {% if uc.role == 'CompanyAdmin' %}selected{% endif %}>Company Admin</option>
{% endif %}
</select>
{% else %}
<span class="badge {% if uc.role == 'CompanyAdmin' %}bg-warning{% else %}bg-info{% endif %}">
{{ uc.role }}
</span>
{% endif %}
</td>
<td>
{% if current_user.role == 'GlobalAdmin' or (current_user.role == 'Admin' and user.role not in ['Admin', 'GlobalAdmin']) %}
<button class="btn btn-sm btn-danger"
onclick="removeFromCompany({{ user.id }}, {{ uc.company_id }})">
Remove
</button>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr id="no-companies-row-{{ user.id }}">
<td colspan="3" class="text-muted text-center">
User is not associated with any companies.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- Add to Company Section -->
{% if current_user.role == 'GlobalAdmin' or (current_user.role == 'Admin' and user.role not in ['Admin', 'GlobalAdmin']) %}
<hr>
<h6>Add to Company:</h6>
<div class="row mb-3">
<div class="col-md-6">
<select class="form-select" id="addCompanySelect{{ user.id }}">
<option value="">Select a company...</option>
<!-- Options will be populated by JavaScript -->
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="addCompanyRole{{ user.id }}">
<option value="User">User</option>
{% if current_user.role == 'GlobalAdmin' %}
<option value="CompanyAdmin">Company Admin</option>
{% endif %}
</select>
</div>
<div class="col-md-3">
<button class="btn btn-primary" onclick="addToCompany({{ user.id }})">
Add to Company
</button>
</div>
</div>
<!-- Create New Company Section (GlobalAdmin only) -->
{% if current_user.role == 'GlobalAdmin' %}
<div class="border-top pt-3">
<h6>Create New Company:</h6>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control"
id="newCompanyName{{ user.id }}"
placeholder="Company name...">
</div>
<div class="col-md-3">
<select class="form-select" id="newCompanyRole{{ user.id }}">
<option value="User">User</option>
<option value="CompanyAdmin">Company Admin</option>
</select>
</div>
<div class="col-md-3">
<button class="btn btn-success" onclick="createAndAddCompany({{ user.id }})">
Create & Add
</button>
</div>
</div>
</div>
{% endif %}
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</td>
<td>
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
{{ 'Active' if user.is_active else 'Inactive' }}
</span>
</td>
<td>
<span class="badge {% if user.mfa_enabled %}bg-success{% else %}bg-secondary{% endif %}">
{{ 'Enabled' if user.mfa_enabled else 'Disabled' }}
</span>
{% if current_user.role == 'GlobalAdmin' %}
<br><small class="text-muted">
Required:
{% if user.mfa_required is none %}
<span class="badge bg-info">Inherit</span>
{% elif user.mfa_required %}
<span class="badge bg-warning">Yes</span>
{% else %}
<span class="badge bg-success">No</span>
{% endif %}
</small>
<br>
<div class="btn-group btn-group-sm mt-1" role="group">
<button type="button" class="btn btn-outline-info btn-xs"
onclick="updateMfaRequirement({{ user.id }}, null)"
{% if user.mfa_required is none %}disabled{% endif %}>
Inherit
</button>
<button type="button" class="btn btn-outline-warning btn-xs"
onclick="updateMfaRequirement({{ user.id }}, true)"
{% if user.mfa_required == true %}disabled{% endif %}>
Required
</button>
<button type="button" class="btn btn-outline-success btn-xs"
onclick="updateMfaRequirement({{ user.id }}, false)"
{% if user.mfa_required == false %}disabled{% endif %}>
Not Required
</button>
</div>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
{% if current_user.is_global_admin() or (current_user.role == 'Admin' and user.role != 'GlobalAdmin' and user.role != 'Admin') %}
<button type="button" class="btn btn-sm btn-warning"
onclick="toggleUserStatus({{ user.id }}, this)"
{% if user.id == current_user.id %}disabled title="You cannot deactivate your own account"{% endif %}>
{{ 'Deactivate' if user.is_active else 'Activate' }}
</button>
<button type="button" class="btn btn-sm btn-info"
onclick="showResetPasswordModal({{ user.id }})"
{% if user.id == current_user.id %}disabled title="Use your profile page to change your own password"{% endif %}>
Reset Password
</button>
{% if user.mfa_secret %}
<form action="{{ url_for('auth.admin_reset_mfa', user_id=user.id) }}"
method="POST" style="display: inline;"
onsubmit="return confirm('Are you sure you want to reset 2FA for this user? They will need to set it up again.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-secondary"
{% if user.id == current_user.id %}disabled title="Use your profile page to manage your own 2FA"{% endif %}>
Reset 2FA
</button>
</form>
{% endif %}
{% if user.id != current_user.id %}
<form action="{{ url_for('auth.delete_user', user_id=user.id) }}"
method="POST" style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this user?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{% endif %}
{% else %}
<button type="button" class="btn btn-sm btn-secondary" disabled title="You don't have permission to manage this user">
No Actions Available
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<!-- Toasts will be inserted here dynamically -->
</div>
<!-- Reset Password Modal -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Reset User Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="resetPasswordForm" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-body">
<div class="mb-3">
<label for="new_password" class="form-label">New Password</label>
<input type="password" class="form-control" id="new_password" name="new_password" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Reset Password</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<script>
// Get current user role for permission checks
const currentUserRole = '{{ current_user.role }}';
// Function to create and display toast notifications
function createToast(type, message) {
const toastContainer = document.querySelector('.toast-container');
// Create toast element
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-white bg-${type} border-0`;
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
// Create toast content
const toastContent = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastEl.innerHTML = toastContent;
toastContainer.appendChild(toastEl);
// Create Bootstrap toast instance
const toast = new bootstrap.Toast(toastEl, {
delay: 5000,
autohide: true
});
// Remove toast element after it's hidden
toastEl.addEventListener('hidden.bs.toast', () => {
toastEl.remove();
});
return toast;
}
function toggleUserStatus(userId, button) {
const baseUrl = '{{ url_for("auth.toggle_user_status", user_id=1) }}'.replace('/1', '/' + userId);
fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => {
if (response.ok) {
return response.json().then(data => {
// Show success message
const toast = createToast('success', 'User status updated successfully');
toast.show();
setTimeout(() => location.reload(), 1000);
});
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to toggle user status');
});
}
})
.catch(error => {
console.error('Error:', error);
const toast = createToast('danger', error.message || 'Failed to toggle user status');
toast.show();
});
}
function showResetPasswordModal(userId) {
const modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
const baseUrl = '{{ url_for("auth.reset_user_password", user_id=1) }}'.replace('/1', '/' + userId);
document.getElementById('resetPasswordForm').action = baseUrl;
modal.show();
}
// Add function to handle changing user roles
function changeUserRole(userId, role) {
if (confirm(`Are you sure you want to change this user's role to ${role}?`)) {
const baseUrl = '{{ url_for("auth.change_user_role", user_id=1) }}'.replace('/1', '/' + userId);
fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ role: role })
})
.then(response => {
if (response.ok) {
location.reload();
} else {
response.json().then(data => {
alert(data.error || 'Failed to change user role');
}).catch(() => {
alert('Failed to change user role');
});
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to change user role');
});
}
}
// Add function to handle MFA requirement updates
function updateMfaRequirement(userId, mfaRequired) {
const actionText = mfaRequired === null ? 'inherit from global setting' :
mfaRequired ? 'require MFA' : 'not require MFA';
if (confirm(`Are you sure you want to set this user to ${actionText}?`)) {
const baseUrl = '{{ url_for("auth.update_user_mfa_requirement", user_id=1) }}'.replace('/1', '/' + userId);
fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ mfa_required: mfaRequired })
})
.then(response => {
if (response.ok) {
return response.json().then(data => {
const toast = createToast('success', data.message);
toast.show();
setTimeout(() => location.reload(), 1000);
});
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to update MFA requirement');
});
}
})
.catch(error => {
console.error('Error:', error);
const toast = createToast('danger', error.message || 'Failed to update MFA requirement');
toast.show();
});
}
}
// Company management functions
function loadAvailableCompanies(userId) {
const select = document.getElementById(`addCompanySelect${userId}`);
// If the select element doesn't exist (e.g., Admin viewing another Admin), skip loading
if (!select) {
console.log(`Add Company select not found for user ${userId} - user likely doesn't have permission to manage this user's companies`);
return;
}
fetch('{{ url_for("auth.get_companies_for_user_management") }}')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
// Clear existing options except the first one
select.innerHTML = '<option value="">Select a company...</option>';
// Handle the response format (companies array is in data.companies)
const companies = data.companies || data || [];
companies.forEach(company => {
const option = document.createElement('option');
option.value = company.id;
option.textContent = company.name;
select.appendChild(option);
});
console.log(`Loaded ${companies.length} companies for user ${userId}`);
})
.catch(error => {
console.error('Error loading companies:', error);
const toast = createToast('danger', `Failed to load available companies: ${error.message}`);
toast.show();
});
}
function addToCompany(userId) {
const companySelect = document.getElementById(`addCompanySelect${userId}`);
const roleSelect = document.getElementById(`addCompanyRole${userId}`);
const companyId = companySelect.value;
const role = roleSelect.value;
if (!companyId) {
const toast = createToast('warning', 'Please select a company');
toast.show();
return;
}
const baseUrl = '{{ url_for("auth.add_user_to_company", user_id=1) }}'.replace('/1', '/' + userId);
fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
company_id: parseInt(companyId),
role: role
})
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to add user to company');
});
}
})
.then(data => {
const toast = createToast('success', data.message);
toast.show();
// Add new row to the current companies table
const tbody = document.getElementById(`currentCompanies${userId}`);
if (tbody) {
// Remove "no companies" row if it exists
const noCompaniesRow = document.getElementById(`no-companies-row-${userId}`);
if (noCompaniesRow) {
noCompaniesRow.remove();
}
const newRow = document.createElement('tr');
newRow.id = `company-row-${userId}-${companyId}`;
newRow.innerHTML = `
<td>${companySelect.options[companySelect.selectedIndex].text}</td>
<td>
<select class="form-select form-select-sm"
onchange="changeCompanyRole(${userId}, ${companyId}, this.value)">
<option value="User" ${role === 'User' ? 'selected' : ''}>User</option>
${currentUserRole === 'GlobalAdmin' ? `<option value="CompanyAdmin" ${role === 'CompanyAdmin' ? 'selected' : ''}>Company Admin</option>` : ''}
</select>
</td>
<td>
<button class="btn btn-sm btn-danger"
onclick="removeFromCompany(${userId}, ${companyId})">
Remove
</button>
</td>
`;
tbody.appendChild(newRow);
}
// Reset the form
companySelect.value = '';
roleSelect.value = 'User';
// Remove the selected company from the dropdown
const selectedOption = companySelect.querySelector(`option[value="${companyId}"]`);
if (selectedOption) {
selectedOption.remove();
}
})
.catch(error => {
console.error('Error:', error);
const toast = createToast('danger', error.message);
toast.show();
});
}
function removeFromCompany(userId, companyId) {
if (!confirm('Are you sure you want to remove this user from the company?')) {
return;
}
const baseUrl = '{{ url_for("auth.remove_user_from_company", user_id=1, company_id=1) }}'.replace('/1/companies/1/', `/${userId}/companies/${companyId}/`);
fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to remove user from company');
});
}
})
.then(data => {
const toast = createToast('success', data.message);
toast.show();
// Remove the row from the table
const row = document.getElementById(`company-row-${userId}-${companyId}`);
if (row) {
const companyName = row.querySelector('td').textContent;
row.remove();
// Check if this was the last company row
const tbody = document.getElementById(`currentCompanies${userId}`);
const remainingRows = tbody.querySelectorAll('tr:not([id^="no-companies-row"])');
if (remainingRows.length === 0) {
// Add the "no companies" row back
const noCompaniesRow = document.createElement('tr');
noCompaniesRow.id = `no-companies-row-${userId}`;
noCompaniesRow.innerHTML = `
<td colspan="3" class="text-muted text-center">
User is not associated with any companies.
</td>
`;
tbody.appendChild(noCompaniesRow);
}
// Add the company back to the dropdown
const select = document.getElementById(`addCompanySelect${userId}`);
const option = document.createElement('option');
option.value = companyId;
option.textContent = companyName;
select.appendChild(option);
}
})
.catch(error => {
console.error('Error:', error);
const toast = createToast('danger', error.message);
toast.show();
});
}
function changeCompanyRole(userId, companyId, newRole) {
const baseUrl = '{{ url_for("auth.change_user_company_role", user_id=1, company_id=1) }}'.replace('/1/companies/1/', `/${userId}/companies/${companyId}/`);
fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ role: newRole })
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to change company role');
});
}
})
.then(data => {
const toast = createToast('success', data.message);
toast.show();
})
.catch(error => {
console.error('Error:', error);
const toast = createToast('danger', error.message);
toast.show();
// Revert the select value on error
location.reload();
});
}
function createAndAddCompany(userId) {
const nameInput = document.getElementById(`newCompanyName${userId}`);
const roleSelect = document.getElementById(`newCompanyRole${userId}`);
const companyName = nameInput.value.trim();
const role = roleSelect.value;
if (!companyName) {
const toast = createToast('warning', 'Please enter a company name');
toast.show();
return;
}
fetch('{{ url_for("auth.create_company_from_manage_users") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
name: companyName,
user_id: userId,
role: role
})
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to create company');
});
}
})
.then(data => {
const toast = createToast('success', data.message);
toast.show();
// Add new row to the current companies table
const tbody = document.getElementById(`currentCompanies${userId}`);
if (tbody) {
// Remove "no companies" row if it exists
const noCompaniesRow = document.getElementById(`no-companies-row-${userId}`);
if (noCompaniesRow) {
noCompaniesRow.remove();
}
const newRow = document.createElement('tr');
newRow.id = `company-row-${userId}-${data.company_id}`;
newRow.innerHTML = `
<td>${companyName}</td>
<td>
<select class="form-select form-select-sm"
onchange="changeCompanyRole(${userId}, ${data.company_id}, this.value)">
<option value="User" ${role === 'User' ? 'selected' : ''}>User</option>
${currentUserRole === 'GlobalAdmin' ? `<option value="CompanyAdmin" ${role === 'CompanyAdmin' ? 'selected' : ''}>Company Admin</option>` : ''}
</select>
</td>
<td>
<button class="btn btn-sm btn-danger"
onclick="removeFromCompany(${userId}, ${data.company_id})">
Remove
</button>
</td>
`;
tbody.appendChild(newRow);
} else {
console.warn(`Table body currentCompanies${userId} not found`);
// Just reload the page if we can't update the table
setTimeout(() => location.reload(), 1000);
}
// Reset the form
nameInput.value = '';
roleSelect.value = 'User';
// Add the new company to all dropdown lists
document.querySelectorAll('[id^="addCompanySelect"]').forEach(select => {
const option = document.createElement('option');
option.value = data.company_id;
option.textContent = companyName;
select.appendChild(option);
});
})
.catch(error => {
console.error('Error:', error);
const toast = createToast('danger', error.message);
toast.show();
});
}
// Load available companies when modal is opened
document.addEventListener('DOMContentLoaded', function() {
// Add event listeners for when company modals are shown
document.querySelectorAll('[id^="companiesModal"]').forEach(modal => {
modal.addEventListener('shown.bs.modal', function() {
const userId = this.id.replace('companiesModal', '');
loadAvailableCompanies(userId);
});
});
});
$(document).ready(function() {
$('#usersTable').DataTable({
"pageLength": 10,
"order": [[0, "asc"]]
});
});
</script>
{% endblock %}
+22
View File
@@ -0,0 +1,22 @@
<!-- MFA Verification Modal -->
<div class="modal fade" id="mfaModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Two-Factor Authentication</h5>
</div>
<form id="mfaForm" method="POST" action="{{ url_for('auth.verify_mfa') }}">
<div class="modal-body">
<div class="mb-3">
<label for="mfa_code" class="form-label">Enter the 6-digit code from your authenticator app</label>
<input type="text" class="form-control" id="mfa_code" name="mfa_code"
required pattern="[0-9]{6}" maxlength="6">
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Verify</button>
</div>
</form>
</div>
</div>
</div>
+62
View File
@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3>Two-Factor Authentication Setup</h3>
{% if forced_setup %}
<div class="alert alert-warning mb-0 mt-2">
<strong>Required:</strong> You must set up two-factor authentication to continue using your account.
</div>
{% endif %}
</div>
<div class="card-body">
<ol class="mb-4">
<li>Install an authenticator app like Google Authenticator or Microsoft Authenticator on your phone</li>
<li>Scan the QR code below or manually enter the secret key in your authenticator app</li>
<li>Enter the 6-digit code from your app to verify setup</li>
</ol>
<div class="text-center mb-4">
<img src="{{ qr_code }}" alt="QR Code" class="img-fluid" style="max-width: 200px;">
</div>
<div class="accordion mb-4">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#manualSetup">
Can't scan the QR code?
</button>
</h2>
<div id="manualSetup" class="accordion-collapse collapse">
<div class="accordion-body">
<p><strong>Secret Key:</strong> <code>{{ secret }}</code></p>
<p class="text-muted small">Enter this secret key manually in your authenticator app if you can't scan the QR code.</p>
</div>
</div>
</div>
</div>
<form method="POST" action="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="verification_code" class="form-label">Verification Code</label>
<input type="text" class="form-control" id="verification_code"
name="verification_code" required pattern="[0-9]{6}" maxlength="6">
</div>
<button type="submit" class="btn btn-primary">Verify and Enable 2FA</button>
{% if not forced_setup %}
<a href="{{ url_for('auth.profile') }}" class="btn btn-secondary">Cancel</a>
{% else %}
<a href="{{ url_for('auth.logout') }}" class="btn btn-secondary">Logout</a>
{% endif %}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+68
View File
@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3>Profile Settings</h3>
</div>
<div class="card-body">
<h5>Account Information</h5>
<p><strong>Username:</strong> {{ current_user.username }}</p>
<p><strong>Email:</strong> {{ current_user.email }}</p>
<p><strong>Role:</strong> {{ current_user.role }}</p>
<hr>
<h5>Two-Factor Authentication</h5>
<p>Status:
<span class="badge {% if current_user.mfa_enabled %}bg-success{% else %}bg-warning{% endif %}">
{{ "Enabled" if current_user.mfa_enabled else "Disabled" }}
</span>
{% if current_user.is_mfa_required() %}
<span class="badge bg-info ms-2">Required</span>
{% endif %}
</p>
<div class="btn-group" role="group">
{% if not current_user.mfa_enabled %}
<a href="{{ url_for('auth.setup_mfa') }}" class="btn btn-primary">Setup 2FA</a>
{% endif %}
{% if current_user.mfa_secret %}
{% if current_user.mfa_enabled and current_user.is_mfa_required() and current_user.role != 'GlobalAdmin' %}
<!-- User cannot disable MFA when it's required, unless they're GlobalAdmin -->
<button type="button" class="btn btn-warning" disabled
title="MFA is required and cannot be disabled. Contact your administrator.">
Disable 2FA (Required)
</button>
{% else %}
<!-- User can toggle MFA -->
<form method="POST" action="{{ url_for('auth.toggle_mfa') }}" style="display: inline;">
{{ mfa_action_form.hidden_tag() }}
<button type="submit" class="btn btn-warning">
{{ "Disable" if current_user.mfa_enabled else "Enable" }} 2FA
</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('auth.reset_mfa') }}" style="display: inline;"
onsubmit="return confirm('Are you sure you want to reset 2FA? You will need to set it up again.');">
{{ mfa_action_form.hidden_tag() }}
<button type="submit" class="btn btn-danger">Reset 2FA</button>
</form>
{% endif %}
</div>
{% if current_user.is_mfa_required() and current_user.role != 'GlobalAdmin' %}
<div class="alert alert-info mt-3">
<small><strong>Note:</strong> MFA is required for your account and cannot be disabled. Contact your administrator if you need to disable MFA.</small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+60
View File
@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-header">
<h3>Register</h3>
</div>
<div class="card-body">
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% if form.username.errors %}
{% for error in form.username.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control") }}
{% if form.confirm_password.errors %}
{% for error in form.confirm_password.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
</div>
<div class="card-footer">
<small>Already have an account? <a href="{{ url_for('auth.login') }}">Sign in</a></small>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
+170
View File
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
{% block supertitle %}{% endblock %}
<title>Domain Logon Monitor - {% block title %}{% endblock %}</title>
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/custom.css') }}" rel="stylesheet">
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.png') }}">
{% block head %}{% endblock %}
<style>
.theme-icon {
width: 1em;
height: 1em;
display: none;
}
[data-bs-theme="dark"] .theme-icon-dark {
display: inline;
}
[data-bs-theme="light"] .theme-icon-light {
display: inline;
}
.dark-theme {
background-color: #212529;
color: #f8f9fa;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('frontend.index') }}">
<img src="{{ url_for('static', filename='img/favicon.png') }}" style="width: 30px;">
<!-- Performance icons created by Uniconlabs - Flaticon ( www.flaticon.com/free-icons/performance) -->
Domain Logon Monitor</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dashboardDropdown" role="button" data-bs-toggle="dropdown">
Dashboard
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('frontend.dashboard') }}">Login Events</a></li>
<li><a class="dropdown-item" href="{{ url_for('frontend.time_spent_report') }}">Time Spent Report</a></li>
</ul>
</li>
{% if current_user.role == 'Admin' %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.manage_users') }}">Manage Users</a>
</li>
{% endif %}
{% if current_user.role == 'GlobalAdmin' %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown">
Admin
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('auth.manage_users') }}">Manage Users</a></li>
<li><a class="dropdown-item" href="{{ url_for('auth.manage_companies') }}">Companies</a></li>
<li><a class="dropdown-item" href="{{ url_for('auth.admin_settings') }}">Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.view_error_logs') }}">Error Logs</a></li>
</ul>
</li>
{% endif %}
{# Companies dropdown for users who belong to multiple companies or company admins #}
{% if current_user.companies|length > 0 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="companyDropdown" role="button" data-bs-toggle="dropdown">
Companies
</a>
<ul class="dropdown-menu">
{% for uc in current_user.companies %}
<li>
<a class="dropdown-item" href="{{ url_for('frontend.dashboard', company_id=uc.company.id) }}">
{{ uc.company.name }}
</a>
</li>
{% if uc.role == 'CompanyAdmin' %}
<li>
<a class="dropdown-item ps-4" href="{{ url_for('auth.company_users', company_id=uc.company.id) }}">
<small>Manage Users</small>
</a>
</li>
<li>
<a class="dropdown-item ps-4" href="{{ url_for('auth.company_api_keys', company_id=uc.company.id) }}">
<small>Sites (API Key)</small>
</a>
</li>
<li><hr class="dropdown-divider"></li>
{% endif %}
{% endfor %}
</ul>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.profile') }}">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
{% if allow_registration %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
</li>
{% endif %}
{% endif %}
<li class="nav-item">
<button class="btn btn-link nav-link" id="theme-toggle">
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</svg>
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
</svg>
</button>
</li>
</ul>
</div>
</div>
</nav>
<main>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} mt-3">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
{% block scripts %}{% endblock %}
<script>
// Theme toggler
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle');
const storedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-bs-theme', storedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
});
</script>
</body>
<!-- <a href="https://www.flaticon.com/free-icons/performance" title="performance icons">Performance icons created by Uniconlabs - Flaticon</a> -->
</html>
+57
View File
@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block supertitle %}
<title>Opsiee {{status_code}} Error</title>
{% endblock %}
{% block content %}
<div class="container mt-4">
<h1 style="color: red; font-weight: bold;">{{status_code}} Error</h1>
<p style="color: deeppink;font-weight: bold;">{{description}}</p>
<br></br>
{% if current_user.is_authenticated %}
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Quick Links:</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<a href="{{ url_for('frontend.dashboard') }}" class="text-decoration-none">View Dashboard</a>
</li>
<li class="list-group-item">
<a href="{{ url_for('frontend.time_spent_report') }}" class="text-decoration-none">Time Spent Report</a>
</li>
{% if current_user.role == 'Admin' or current_user.role == 'GlobalAdmin' %}
<br></br><h5 class="card-title">Management:</h5>
<li class="list-group-item">
<a href="{{ url_for('auth.manage_users') }}" class="text-decoration-none">Manage Users</a>
</li>
{% endif %}
{% if current_user.role == 'GlobalAdmin' %}
<li class="list-group-item">
<a href="{{ url_for('auth.manage_companies') }}" class="text-decoration-none">Manage Companies</a>
</li>
<li class="list-group-item">
<a href="{{ url_for('auth.admin_settings') }}" class="text-decoration-none">Site Settings</a>
</li>
<li class="list-group-item">
<a href="{{ url_for('auth.view_error_logs') }}" class="text-decoration-none">View Error Logs</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
{% else %}
<p class="lead">Please login {% if allow_registration %}or register {% endif %}to access the monitoring system.</p>
<div class="row mt-4">
<div class="col-md-6">
<a href="{{ url_for('auth.login') }}" class="btn btn-primary mr-2">Login</a>
{% if allow_registration %}
<a href="{{ url_for('auth.register') }}" class="btn btn-secondary">Register</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock content %}
+459
View File
@@ -0,0 +1,459 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.dataTables.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/colVis.dataTables.min.css') }}" rel="stylesheet">
<!-- DateRangePicker CSS -->
<link href="{{ url_for('static', filename='css/daterangepicker.css') }}" rel="stylesheet">
<!-- Custom DateRangePicker dark theme styles -->
<style>
/* Dark theme for date picker */
.daterangepicker {
background-color: #212529;
border-color: #495057;
color: #f8f9fa;
}
.daterangepicker .calendar-table {
background-color: #343a40;
border-color: #495057;
}
.daterangepicker td.available:hover,
.daterangepicker th.available:hover {
background-color: #495057;
}
.daterangepicker td.active,
.daterangepicker td.active:hover {
background-color: #0d6efd;
color: #fff;
}
/* In-between dates in the selected range - lighter blue */
.daterangepicker td.in-range {
background-color: #82b1ff; /* Lighter blue */
color: #212529; /* Darker text for better contrast */
}
.daterangepicker td.in-range:hover {
background-color: #75a7f7; /* Slightly darker when hovering */
color: #212529;
}
.daterangepicker .calendar-table .next span,
.daterangepicker .calendar-table .prev span {
border-color: #f8f9fa;
}
.daterangepicker .ranges li:hover,
.daterangepicker .ranges li.active {
background-color: #0d6efd;
color: #fff;
}
.daterangepicker .ranges li {
color: #f8f9fa;
}
.daterangepicker:after {
border-bottom-color: #212529;
}
.daterangepicker:before {
border-bottom-color: #495057;
}
/* Calendar header and weekday styling */
.daterangepicker .calendar-table th {
color: #f8f9fa;
}
/* Month name */
.daterangepicker .month {
color: #f8f9fa;
}
/* Off days (not in current month) */
.daterangepicker td.off {
color: #6c757d;
}
/* Input boxes */
.daterangepicker input.input-mini {
background-color: #343a40;
border-color: #495057;
color: #f8f9fa;
}
/* Time picker */
.daterangepicker .calendar-time select {
background-color: #343a40;
border-color: #495057;
color: #f8f9fa;
}
/* Apply and cancel buttons */
.daterangepicker .drp-buttons {
border-top-color: #495057;
}
.daterangepicker .drp-buttons .btn {
color: #f8f9fa;
}
/* Input fields focus */
.daterangepicker input.input-mini:focus {
border-color: #0d6efd;
}
/* Time inputs container */
.daterangepicker .calendar-time {
background-color: #343a40;
border-color: #495057;
}
/* Make the export buttons more visible */
.dt-buttons {
margin-top: 10px;
margin-bottom: 15px;
display: inline-block !important;
}
.dt-button {
margin-right: 5px;
}
/* Page title and export buttons container */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
}
.page-title {
margin-bottom: 0;
}
/* For smaller screens */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
}
.export-buttons {
margin-top: 10px;
}
}
/* Column visibility dropdown styles */
.dropdown-menu {
background-color: #343a40;
border-color: #495057;
}
.dropdown-item {
color: #f8f9fa;
}
.dropdown-item:hover, .dropdown-item:focus {
background-color: #495057;
color: #f8f9fa;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.form-check-label {
color: #f8f9fa;
}
#column-visibility-menu {
min-width: 200px;
}
.column-checkbox {
padding: 0.375rem 1rem;
}
</style>
{% endblock %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="page-header">
<h2 class="page-title">Login Events Dashboard</h2>
<div class="export-buttons">
<div class="btn-group me-2" role="group">
<button id="column-visibility" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Columns
</button>
<ul class="dropdown-menu" id="column-visibility-menu">
<!-- Column visibility checkboxes will be populated by JavaScript -->
</ul>
</div>
<button id="export-csv" class="btn btn-secondary btn-sm">Export CSV</button>
<button id="export-excel" class="btn btn-success btn-sm">Export Excel</button>
<button id="print-table" class="btn btn-info btn-sm">Print</button>
</div>
</div>
<div class="card mt-3 mb-3">
<div class="card-body">
<form id="dateRangeForm" method="GET" action="{{ url_for('frontend.dashboard') }}">
<div class="row align-items-end">
<div class="col-md-3">
<label for="daterange" class="form-label">Date Range:</label>
<input type="text" id="daterange" name="daterange" class="form-control"
value="{{ start_date.strftime('%Y-%m-%d %H:%M') if start_date else '' }} - {{ end_date.strftime('%Y-%m-%d %H:%M') if end_date else '' }}"/>
</div>
{% if companies %}
<div class="col-md-3">
<label for="company_id" class="form-label">Company:</label>
<select class="form-select" id="company_id" name="company_id">
<option value="">All Companies</option>
{% for company in companies %}
<option value="{{ company.id }}" {% if selected_company_id == company.id %}selected{% endif %}>
{{ company.name }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Apply Filter</button>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<table id="logsTable" class="table table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>Event Type</th>
<th>User Name</th>
<th>Computer Name</th>
<th>IP Address</th>
<th>Site</th>
<th>Timestamp</th>
{% if current_user.is_global_admin() and not selected_company_id %}
<th>Company</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.event_type }}</td>
<td>{{ log.user_name }}</td>
<td>{{ log.computer_name }}</td>
<td>{{ log.ip_address }}</td>
<td>{{ log.api_key.description if log.api_key else 'N/A' }}</td>
<td data-order="{{ log.timestamp.strftime('%Y%m%d%H%M%S') }}">
{{ log.timestamp|format_datetime }}
</td>
{% if current_user.is_global_admin() and not selected_company_id %}
<td>{{ log.company.name if log.company else 'N/A' }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<!-- DataTables Buttons JS -->
<script src="{{ url_for('static', filename='js/dataTables.buttons.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.bootstrap5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.html5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.print.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/jszip.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/pdfmake.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/vfs_fonts.js') }}"></script>
<!-- Moment.js -->
<script src="{{ url_for('static', filename='js/moment.min.js') }}"></script>
<!-- DateRangePicker -->
<script src="{{ url_for('static', filename='js/daterangepicker.min.js') }}"></script>
<script>
$(document).ready(function() {
$('#daterange').daterangepicker({
timePicker: true,
timePicker24Hour: true,
timePickerSeconds: false,
timePickerIncrement: 1, // Changed from 15 to 1 minute increments
autoUpdateInput: false, // Prevents auto-update so user can edit manually
locale: {
format: 'YYYY-MM-DD HH:mm',
cancelLabel: 'Clear',
applyLabel: 'Apply'
},
ranges: {
'Last 48 Hours': [moment().subtract(48, 'hours'), moment()],
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
});
// Handle manual input updates
$('#daterange').on('apply.daterangepicker', function(ev, picker) {
$(this).val(picker.startDate.format('YYYY-MM-DD HH:mm') + ' - ' + picker.endDate.format('YYYY-MM-DD HH:mm'));
});
$('#daterange').on('cancel.daterangepicker', function(ev, picker) {
$(this).val('');
});
// Allow direct editing of the date input
$('#daterange').on('keyup', function(e) {
if(e.keyCode === 13) {
// Try to parse the input value
var parts = $(this).val().split(' - ');
if(parts.length === 2) {
var startDate = moment(parts[0], 'YYYY-MM-DD HH:mm');
var endDate = moment(parts[1], 'YYYY-MM-DD HH:mm');
if(startDate.isValid() && endDate.isValid()) {
var picker = $(this).data('daterangepicker');
picker.setStartDate(startDate);
picker.setEndDate(endDate);
}
}
}
});
var table = $('#logsTable').DataTable({
pageLength: 50,
lengthMenu: [[50, 100, 200, 500, 1000], [50, 100, 200, 500, 1000]],
order: [[{% if current_user.is_global_admin() and not selected_company_id %}6{% else %}5{% endif %}, 'desc']], // Sort by timestamp column descending
dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
columnDefs: [
{
targets: [2, 3], // Computer Name and IP Address columns
visible: false // Hide by default
}
],
buttons: [
{
extend: 'csv',
text: 'Export CSV',
className: 'btn btn-secondary',
filename: 'login_events_' + moment().format('YYYY-MM-DD'),
exportOptions: {
columns: ':visible'
}
},
{
extend: 'excel',
text: 'Export Excel',
className: 'btn btn-success',
filename: 'login_events_' + moment().format('YYYY-MM-DD'),
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
className: 'btn btn-info',
exportOptions: {
columns: ':visible'
}
}
],
language: {
search: "Search records:",
lengthMenu: "Show _MENU_ records per page",
info: "Showing _START_ to _END_ of _TOTAL_ records",
paginate: {
first: "First",
last: "Last",
next: "Next",
previous: "Previous"
}
}
});
// Column names for the visibility controls
var columnNames = [
'Event Type',
'User Name',
'Computer Name',
'IP Address',
'Site',
'Timestamp'
{% if current_user.is_global_admin() and not selected_company_id %},
'Company'
{% endif %}
];
// Load saved column visibility from localStorage
function loadColumnVisibility() {
var saved = localStorage.getItem('dashboardColumnVisibility');
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
console.log('Error parsing saved column visibility:', e);
}
}
// Default visibility - hide Computer Name (2) and IP Address (3)
var defaultVisibility = {};
columnNames.forEach(function(name, index) {
defaultVisibility[index] = index !== 2 && index !== 3;
});
return defaultVisibility;
}
// Save column visibility to localStorage
function saveColumnVisibility(visibility) {
localStorage.setItem('dashboardColumnVisibility', JSON.stringify(visibility));
}
// Apply saved column visibility to table
var savedVisibility = loadColumnVisibility();
Object.keys(savedVisibility).forEach(function(colIndex) {
table.column(parseInt(colIndex)).visible(savedVisibility[colIndex]);
});
// Create column visibility dropdown menu
function createColumnVisibilityMenu() {
var menu = $('#column-visibility-menu');
menu.empty();
columnNames.forEach(function(columnName, index) {
var isVisible = table.column(index).visible();
var checkboxId = 'col-vis-' + index;
var menuItem = $('<li class="column-checkbox"></li>');
var formCheck = $('<div class="form-check"></div>');
var checkbox = $('<input class="form-check-input" type="checkbox" id="' + checkboxId + '"' +
(isVisible ? ' checked' : '') + '>');
var label = $('<label class="form-check-label" for="' + checkboxId + '">' + columnName + '</label>');
checkbox.on('change', function() {
var colIndex = parseInt(this.id.split('-')[2]);
var isChecked = this.checked;
table.column(colIndex).visible(isChecked);
// Update saved visibility
savedVisibility[colIndex] = isChecked;
saveColumnVisibility(savedVisibility);
});
formCheck.append(checkbox, label);
menuItem.append(formCheck);
menu.append(menuItem);
});
}
// Initialize column visibility menu
createColumnVisibilityMenu();
// Prevent dropdown from closing when clicking inside
$('#column-visibility-menu').on('click', function(e) {
e.stopPropagation();
});
// Connect the custom export buttons to DataTables buttons
$('#export-csv').on('click', function() {
table.button('.buttons-csv').trigger();
});
$('#export-excel').on('click', function() {
table.button('.buttons-excel').trigger();
});
$('#print-table').on('click', function() {
table.button('.buttons-print').trigger();
});
});
</script>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h1>Welcome to Domain Logon Monitoring</h1>
{% if current_user.is_authenticated %}
<p class="lead">Hello {{ current_user.username }}, welcome back!</p>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Quick Links:</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item">
<a href="{{ url_for('frontend.dashboard') }}" class="text-decoration-none">View Dashboard</a>
</li>
<li class="list-group-item">
<a href="{{ url_for('frontend.time_spent_report') }}" class="text-decoration-none">Time Spent Report</a>
</li>
{% if current_user.role == 'Admin' or current_user.role == 'GlobalAdmin' %}
<br></br><h5 class="card-title">Management:</h5>
<li class="list-group-item">
<a href="{{ url_for('auth.manage_users') }}" class="text-decoration-none">Manage Users</a>
</li>
{% endif %}
{% if current_user.role == 'GlobalAdmin' %}
<li class="list-group-item">
<a href="{{ url_for('auth.manage_companies') }}" class="text-decoration-none">Manage Companies</a>
</li>
<li class="list-group-item">
<a href="{{ url_for('auth.admin_settings') }}" class="text-decoration-none">Site Settings</a>
</li>
<li class="list-group-item">
<a href="{{ url_for('auth.view_error_logs') }}" class="text-decoration-none">View Error Logs</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
{% else %}
<p class="lead">Please login {% if allow_registration %}or register {% endif %}to access the monitoring system.</p>
<div class="row mt-4">
<div class="col-md-6">
<a href="{{ url_for('auth.login') }}" class="btn btn-primary mr-2">Login</a>
{% if allow_registration %}
<a href="{{ url_for('auth.register') }}" class="btn btn-secondary">Register</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock content %}
+302
View File
@@ -0,0 +1,302 @@
{% extends "base.html" %}
{% block head %}
<!-- DataTables CSS -->
<link href="{{ url_for('static', filename='css/dataTables.bootstrap5.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/buttons.bootstrap5.min.css') }}" rel="stylesheet">
<!-- DateRangePicker CSS -->
<link href="{{ url_for('static', filename='css/daterangepicker.css') }}" rel="stylesheet">
<style>
.btn-group-toggle .btn {
margin-right: 5px;
}
.dataTables_wrapper .dt-buttons {
margin-bottom: 15px;
}
.dt-button {
margin-right: 5px;
}
/* Custom DateRangePicker dark theme styles */
.daterangepicker {
background-color: #212529;
border-color: #495057;
color: #f8f9fa;
}
.daterangepicker .calendar-table {
background-color: #343a40;
border-color: #495057;
}
.daterangepicker td.available:hover,
.daterangepicker th.available:hover {
background-color: #495057;
}
.daterangepicker td.active,
.daterangepicker td.active:hover {
background-color: #0d6efd;
color: #fff;
}
/* In-between dates in the selected range - lighter blue */
.daterangepicker td.in-range {
background-color: #82b1ff; /* Lighter blue */
color: #212529; /* Darker text for better contrast */
}
.daterangepicker td.in-range:hover {
background-color: #75a7f7; /* Slightly darker when hovering */
color: #212529;
}
.daterangepicker .calendar-table .next span,
.daterangepicker .calendar-table .prev span {
border-color: #f8f9fa;
}
.daterangepicker .ranges li:hover,
.daterangepicker .ranges li.active {
background-color: #0d6efd;
color: #fff;
}
.daterangepicker .ranges li {
color: #f8f9fa;
}
.daterangepicker:after {
border-bottom-color: #212529;
}
.daterangepicker:before {
border-bottom-color: #495057;
}
/* Calendar header and weekday styling */
.daterangepicker .calendar-table th {
color: #f8f9fa;
}
/* Month name */
.daterangepicker .month {
color: #f8f9fa;
}
/* Off days (not in current month) */
.daterangepicker td.off {
color: #6c757d;
}
/* Input boxes */
.daterangepicker input.input-mini {
background-color: #343a40;
border-color: #495057;
color: #f8f9fa;
}
/* Time picker */
.daterangepicker .calendar-time select {
background-color: #343a40;
border-color: #495057;
color: #f8f9fa;
}
/* Apply and cancel buttons */
.daterangepicker .drp-buttons {
border-top-color: #495057;
}
.daterangepicker .drp-buttons .btn {
color: #f8f9fa;
}
/* Input fields focus */
.daterangepicker input.input-mini:focus {
border-color: #0d6efd;
}
/* Time inputs container */
.daterangepicker .calendar-time {
background-color: #343a40;
border-color: #495057;
}
/* Make the export buttons more visible */
.dt-buttons {
margin-top: 10px;
margin-bottom: 15px;
display: block !important;
}
.dt-button {
margin-right: 5px;
}
/* Align the "Show entries" and search box in the same row */
div.dataTables_wrapper div.dataTables_length {
float: left;
padding-top: 0.5em;
}
div.dataTables_wrapper div.dataTables_filter {
float: right;
}
div.dataTables_wrapper div.dataTables_info {
clear: both;
}
</style>
{% endblock %}
{% block title %}Time Spent Report{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center">
<h2>Time Spent Report</h2>
<div class="export-buttons d-flex align-items-center">
<!-- Process span option moved here -->
<div class="form-check me-3">
<input type="checkbox" class="form-check-input" name="continue_iterate" id="continue_iterate" form="reportFilterForm" {% if continue_iterate %}checked{% endif %}>
<label class="form-check-label" for="continue_iterate" data-bs-toggle="tooltip" data-bs-placement="top"
title="When enabled, the system will continue processing time spans across midnight for multi-day sessions. When disabled, each day's activity is calculated separately.">
Process multi-day sessions
</label>
</div>
<!-- Export buttons will be placed here by DataTables -->
</div>
</div>
<div class="card mt-3 mb-3">
<div class="card-body">
<form id="reportFilterForm" method="GET" action="{{ url_for('frontend.time_spent_report') }}">
<div class="row align-items-end">
<div class="col-md-3">
<label for="daterange" class="form-label">Date Range:</label>
<input type="text" id="daterange" name="daterange" class="form-control"
value="{{ start_date.strftime('%Y-%m-%d %H:%M') if start_date else '' }} - {{ end_date.strftime('%Y-%m-%d %H:%M') if end_date else '' }}"/>
</div>
{% if companies %}
<div class="col-md-2">
<label for="company_id" class="form-label">Company:</label>
<select class="form-select" id="company_id" name="company_id">
<option value="">All Companies</option>
{% for company in companies %}
<option value="{{ company.id }}" {% if selected_company_id == company.id %}selected{% endif %}>
{{ company.name }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="col-md-3">
<label for="group_by" class="form-label">Group By:</label>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<input type="radio" class="btn-check" name="group_by" id="option1" value="user" {% if group_by == 'user' or not group_by %}checked{% endif %}>
<label class="btn btn-outline-primary btn-sm" for="option1">User</label>
<input type="radio" class="btn-check" name="group_by" id="option2" value="user_computer" {% if group_by == 'user_computer' %}checked{% endif %}>
<label class="btn btn-outline-primary btn-sm" for="option2">User + Computer</label>
</div>
</div>
<div class="col-md-2 mt-3">
<button type="submit" class="btn btn-primary">Apply Filter</button>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<table id="timeSpentTable" class="table table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>Date</th>
<th>User Name</th>
{% if group_by == 'user_computer' %}
<th>Computer Name</th>
{% endif %}
<th>Company</th>
<th>Total Time</th>
<th>First Login</th>
<th>Last Logout</th>
</tr>
</thead>
<tbody>
{% for entry in time_data %}
<tr>
<td>{{ entry.date }}</td>
<td>{{ entry.user_name }}</td>
{% if group_by == 'user_computer' %}
<td>{{ entry.computer_name }}</td>
{% endif %}
<td>{{ entry.company_name }}</td>
<td data-order="{{ entry.total_seconds }}">{{ entry.formatted_time }}</td>
<td data-order="{{ entry.first_login.strftime('%Y%m%d%H%M%S') if entry.first_login else '' }}">
{{ entry.first_login|format_datetime if entry.first_login else 'N/A' }}
</td>
<td data-order="{{ entry.last_logout.strftime('%Y%m%d%H%M%S') if entry.last_logout else '' }}">
{{ entry.last_logout|format_datetime if entry.last_logout else 'N/A' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- DataTables JS -->
<script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.bootstrap5.min.js') }}"></script>
<!-- DataTables Buttons JS -->
<script src="{{ url_for('static', filename='js/dataTables.buttons.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.bootstrap5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.html5.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/buttons.print.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/jszip.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/pdfmake.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/vfs_fonts.js') }}"></script>
<!-- Moment.js -->
<script src="{{ url_for('static', filename='js/moment.min.js') }}"></script>
<!-- DateRangePicker -->
<script src="{{ url_for('static', filename='js/daterangepicker.min.js') }}"></script>
<script>
$(document).ready(function() {
// Initialize date range picker
$('#daterange').daterangepicker({
timePicker: true,
timePicker24Hour: true,
timePickerSeconds: false,
startDate: moment().subtract(7, 'days'),
endDate: moment(),
locale: {
format: 'YYYY-MM-DD HH:mm'
}
});
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Initialize DataTable with export buttons
var table = $('#timeSpentTable').DataTable({
dom: 'lfrBtip',
buttons: [
{
extend: 'csv',
text: 'Export CSV',
className: 'btn btn-primary btn-sm',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'excel',
text: 'Export Excel',
className: 'btn btn-success btn-sm',
exportOptions: {
columns: ':visible'
}
},
{
extend: 'print',
text: 'Print',
className: 'btn btn-info btn-sm',
exportOptions: {
columns: ':visible'
}
}
],
order: [[0, 'desc']],
pageLength: 25
});
// Move export buttons to the header
table.buttons().container().appendTo('.export-buttons');
});
</script>
{% endblock %}