mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-02-19 03:58:37 -06:00
feat(core/auth/ui): proxy-aware config, optional OIDC, i18n v4, health checks
- core: add ProxyFix, robust logging setup, rate-limit defaults; mask DB URL in logs
- db: prefer Postgres when POSTGRES_* envs present; initialization helpers and safe task table migration check
- i18n: upgrade to Flask-Babel v4 with locale selector; compile catalogs; add set-language route
- auth: optional OIDC via Authlib (login, callback, logout); login rate limiting; profile language and theme persistence; ensure admin promotion
- admin: branding logo upload/serve; PDF layout editor with preview/reset; backup/restore with progress; system info; license-server controls
- ui: new base layout with improved nav, mobile tab bar, theme/density toggles, CSRF meta + auto-injection, DataTables/Chart.js, Socket.IO boot
- ops: add /_health and /_ready endpoints; Docker healthcheck targets /_health; enable top-level templates via ChoiceLoader
- deps: update/add Authlib, Flask-Babel 4, and related security/util packages
Refs: app/__init__.py, app/config.py, app/routes/{auth,admin,main}.py, app/templates/base.html, Dockerfile, requirements.txt, templates/*
197 lines
9.8 KiB
HTML
197 lines
9.8 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ (user and _('Edit') or _('New')) }} {{ _('User') }} - {{ app_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
{% from "_components.html" import page_header %}
|
|
<div class="row">
|
|
<div class="col-12">
|
|
{% set actions %}
|
|
<a href="{{ url_for('admin.list_users') }}" class="btn-header btn-outline-primary">
|
|
<i class="fas fa-arrow-left"></i> {{ _('Back to Users') }}
|
|
</a>
|
|
{% endset %}
|
|
{{ page_header('fas fa-user', (user and _('Edit User') or _('New User')), _('Create or update user accounts'), actions) }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-edit"></i> {{ _('User Information') }}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="username" class="form-label">{{ _('Username') }}</label>
|
|
{% if user %}
|
|
<input type="text" id="username" class="form-control" value="{{ user.username }}" disabled>
|
|
<input type="hidden" name="username" value="{{ user.username }}">
|
|
{% else %}
|
|
<input type="text" id="username" name="username" class="form-control" value="{{ request.form.get('username', '') }}" required>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="role" class="form-label">{{ _('Role') }}</label>
|
|
<select id="role" name="role" class="form-select">
|
|
{% set selected_role = (user.role if user else request.form.get('role', 'user')) %}
|
|
<option value="user" {% if selected_role == 'user' %}selected{% endif %}>{{ _('User') }}</option>
|
|
<option value="admin" {% if selected_role == 'admin' %}selected{% endif %}>{{ _('Admin') }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
{% set active_checked = (user.is_active if user else (request.form.get('is_active', 'on') in ['on','true','1'])) %}
|
|
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" {% if active_checked %}checked{% endif %}>
|
|
<label class="form-check-label" for="is_active">{{ _('Active') }}</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between">
|
|
<a href="{{ url_for('admin.list_users') }}" class="btn btn-secondary">
|
|
<i class="fas fa-times"></i> {{ _('Cancel') }}
|
|
</a>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save"></i>
|
|
{% if user %}{{ _('Update User') }}{% else %}{{ _('Create User') }}{% endif %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-info-circle"></i> {{ _('Help') }}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<h6>{{ _('Username') }}</h6>
|
|
<p class="text-muted small">{{ _('Choose a unique username for the user. This will be used for login.') }}</p>
|
|
|
|
<h6>{{ _('Role') }}</h6>
|
|
<p class="text-muted small">
|
|
<strong>{{ _('User:') }}</strong> {{ _('Can track time, view projects, and generate reports.') }}<br>
|
|
<strong>{{ _('Admin:') }}</strong> {{ _('Can manage users, projects, and system settings.') }}
|
|
</p>
|
|
|
|
<h6>{{ _('Active Status') }}</h6>
|
|
<p class="text-muted small">{{ _('Inactive users cannot log in or access the system.') }}</p>
|
|
|
|
{% if user %}
|
|
<hr>
|
|
<h6>{{ _('User Statistics') }}</h6>
|
|
<div class="row text-center">
|
|
<div class="col-6">
|
|
<div class="h5 text-primary">{{ "%.1f"|format(user.total_hours) }}</div>
|
|
<small class="text-muted">{{ _('Total Hours') }}</small>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="h5 text-success">{{ user.time_entries.count() }}</div>
|
|
<small class="text-muted">{{ _('Time Entries') }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<h6>{{ _('Account Information') }}</h6>
|
|
<small class="text-muted">
|
|
<div>{{ _('Created:') }} {{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</div>
|
|
{% if user.last_login %}
|
|
<div>{{ _('Last Login:') }} {{ user.last_login.strftime('%Y-%m-%d %H:%M') }}</div>
|
|
{% else %}
|
|
<div>{{ _('Last Login: Never') }}</div>
|
|
{% endif %}
|
|
</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if user and user.id != current_user.id %}
|
|
<div class="card mt-3 border-danger">
|
|
<div class="card-header bg-danger text-white">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-exclamation-triangle"></i> {{ _('Danger Zone') }}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="text-muted small">{{ _('These actions cannot be undone.') }}</p>
|
|
<button type="button" class="btn btn-danger btn-sm"
|
|
onclick="showDeleteUserModal('{{ user.id }}', '{{ user.username }}')">
|
|
<i class="fas fa-trash"></i> {{ _('Delete User') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete User Modal -->
|
|
<div class="modal fade" id="deleteUserModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-trash me-2 text-danger"></i>{{ _('Delete User') }}
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>{{ _('Warning:') }}</strong> {{ _('This action cannot be undone.') }}
|
|
</div>
|
|
<p>{{ _('Are you sure you want to delete the user') }} <strong id="deleteUserName"></strong>?</p>
|
|
<p class="text-muted mb-0">{{ _('This will permanently remove the user and all their data cannot be recovered.') }}</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
|
|
</button>
|
|
<form method="POST" id="deleteUserForm" class="d-inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
{{ csrf_token() }}
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="fas fa-trash me-2"></i>{{ _('Delete User') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Function to show delete user modal
|
|
function showDeleteUserModal(userId, username) {
|
|
document.getElementById('deleteUserName').textContent = username;
|
|
document.getElementById('deleteUserForm').action = "{{ url_for('admin.delete_user', user_id=0) }}".replace('0', userId);
|
|
new bootstrap.Modal(document.getElementById('deleteUserModal')).show();
|
|
}
|
|
|
|
// Add loading state to delete user form
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.getElementById('deleteUserForm').addEventListener('submit', function(e) {
|
|
const submitBtn = this.querySelector('button[type="submit"]');
|
|
submitBtn.innerHTML = '<div class="loading-spinner me-2"></div>Deleting...';
|
|
submitBtn.disabled = true;
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|