feat: Add iOS-specific enhancements and mobile compatibility utilities
- Implemented `remove_key` function in verify.py to allow key removal by user ID. - Updated run.sh to include additional dependencies for QR code and TOTP support. - Introduced ios_fixes.js for iOS-specific enhancements including file input handling, form submission fixes, and image handling optimizations. - Added mobile_compatibility.js for mobile device detection and utility functions to improve compatibility, especially for iOS. - Created base.html template for consistent layout and styling across the application. - Developed login.html template with a password toggle feature for better user experience.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<!--
|
||||
Copyright 2025 Maximilian Gründinger
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>{% block title %}Admin Inventarsystem{% endblock %}</title>
|
||||
{% block head %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--c-bg:#f4f6f9;--c-bg-alt:#ffffff;--c-surface:#ffffff;--c-surface-alt:#f1f5f9;--c-border:#d9dde3;--c-border-strong:#b7bcc4;--c-text:#1f2933;--c-text-soft:#4b5563;--c-text-faint:#6b7280;--c-accent:#2563eb;--c-accent-hover:#1d4ed8;--c-accent-active:#1942b3;--c-success:#059669;--c-success-hover:#047857;--c-danger:#dc2626;--c-danger-hover:#b91c1c;--c-warning:#d97706;--c-focus-ring:0 0 0 3px rgba(37,99,235,.35);--c-shadow-sm:0 1px 3px rgba(0,0,0,0.08);--c-shadow-md:0 4px 12px rgba(0,0,0,0.12);--c-shadow-lg:0 8px 28px rgba(0,0,0,0.18);--c-scroll-track:#e5e7eb;--c-scroll-thumb:#c0c6ce;--c-scroll-thumb-hover:#9aa2ac;--c-badge-bg:#eef2ff;--c-badge-text:#3730a3;--c-code-bg:#f8fafc;--c-overlay:rgba(17,24,39,0.45);--c-link:#2563eb;--c-link-hover:#1d4ed8;--radius-sm:6px;--radius-md:10px;--radius-lg:14px;--transition:.18s cubic-bezier(.4,0,.2,1)
|
||||
}
|
||||
@media (prefers-color-scheme: dark){:root{--c-bg:#0d1117;--c-bg-alt:#101723;--c-surface:#121c29;--c-surface-alt:#182432;--c-border:#2a3a49;--c-border-strong:#3a4d5e;--c-text:#e3e9f1;--c-text-soft:#c2ccd6;--c-text-faint:#94a3b4;--c-accent:#3b82f6;--c-accent-hover:#2563eb;--c-accent-active:#1d4ed8;--c-success:#10b981;--c-success-hover:#059669;--c-danger:#ef4444;--c-danger-hover:#dc2626;--c-warning:#f59e0b;--c-focus-ring:0 0 0 3px rgba(59,130,246,.45);--c-shadow-sm:0 1px 4px rgba(0,0,0,0.55);--c-shadow-md:0 4px 18px rgba(0,0,0,0.55);--c-shadow-lg:0 10px 38px rgba(0,0,0,0.6);--c-scroll-track:#1c2632;--c-scroll-thumb:#314254;--c-scroll-thumb-hover:#44576a;--c-badge-bg:#1e293b;--c-badge-text:#c7d2fe;--c-code-bg:#182432;--c-overlay:rgba(11,18,32,0.62);--c-link:#60a5fa;--c-link-hover:#3b82f6}}
|
||||
html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body{min-height:100svh;margin:0;font-family:Arial,system-ui,sans-serif;background:var(--c-bg);color:var(--c-text);padding:0 0 30px 0;transition:background .3s,color .3s}
|
||||
.container:not(.container-fluid){width:min(100% - 2rem,1200px);margin-inline:auto;padding-top:20px}
|
||||
.fit-content{width:fit-content;max-width:100%}.fit-content-height{height:fit-content;max-height:100%}
|
||||
:where(p,h1,h2,h3,h4,h5,h6,li){overflow-wrap:anywhere;word-break:normal}img,video,canvas,svg{max-width:100%;height:auto}
|
||||
.table-wrap{max-width:100%;overflow-x:auto}table.table-fit{width:max-content;max-width:100%;border-collapse:collapse}table.table-fit th,table.table-fit td{padding:.5rem .75rem;text-align:left}
|
||||
input,select,textarea,button{max-width:100%}
|
||||
/* Navbar */
|
||||
.navbar{box-shadow:0 2px 5px rgba(0,0,0,.1);border-bottom:1px solid var(--c-border);background:linear-gradient(90deg,#111827,#142033)}.navbar-brand{font-weight:600;font-size:1.3rem}
|
||||
/* High contrast on dark navbar in all modes */
|
||||
.navbar-dark .navbar-brand{color:#f3f4f6}
|
||||
.navbar-dark .nav-link{color:#e5e7eb}
|
||||
.navbar-dark .nav-link:hover{color:#ffffff}
|
||||
.admin-nav-section{margin-left:auto;display:flex;gap:15px}
|
||||
.admin-nav-section .nav-item{background:var(--c-success);color:#fff;padding:5px 10px;border-radius:6px;text-decoration:none;transition:var(--transition);font-weight:600;font-size:.85rem}
|
||||
.admin-nav-section .nav-item:hover{background:var(--c-success-hover);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.18)}
|
||||
.navbar-nav{align-items:center}.navbar-nav .nav-item{margin-right:5px}.navbar-nav .nav-item:last-child{margin-right:0}
|
||||
/* Flash messages */
|
||||
.flashes{margin:20px 0}.flash{padding:12px 20px;margin-bottom:15px;border-radius:8px;font-weight:500;border-left:6px solid transparent;background:var(--c-surface);border:1px solid var(--c-border)}
|
||||
.flash.success{background:rgba(16,185,129,0.12);border-left-color:var(--c-success);color:var(--c-success)}
|
||||
.flash.error{background:rgba(239,68,68,0.15);border-left-color:var(--c-danger);color:var(--c-danger)}
|
||||
.flash.info{background:rgba(59,130,246,0.12);border-left-color:var(--c-accent);color:var(--c-accent)}
|
||||
/* Dropdown */
|
||||
.dropdown-menu{box-shadow:0 8px 24px rgba(0,0,0,.15);border:1px solid var(--c-border);border-radius:10px;padding:8px 0;min-width:200px;margin-top:6px;background:var(--c-surface);color:var(--c-text)}
|
||||
.dropdown-item{padding:10px 16px;font-size:.9rem;color:var(--c-text-soft);transition:var(--transition)}
|
||||
.dropdown-item:hover,.dropdown-item:focus{background:var(--c-surface-alt);color:var(--c-text);transform:translateX(2px)}
|
||||
.dropdown-divider{margin:6px 0;border-color:var(--c-border)}
|
||||
/* Logout */
|
||||
.logout-btn{border-radius:999px;padding:.375rem .85rem;font-weight:600;letter-spacing:.015em;line-height:1.25;transition:var(--transition)}
|
||||
.navbar-dark .logout-btn.btn-outline-danger{color:#ffc9ce;border-color:rgba(220,53,69,.6)}
|
||||
.navbar-dark .logout-btn.btn-outline-danger:hover{color:#fff;background:rgba(220,53,69,.9);border-color:rgba(220,53,69,1);transform:translateY(-1px);box-shadow:0 6px 16px rgba(220,53,69,.25)}
|
||||
/* Scrollbars */
|
||||
*::-webkit-scrollbar{width:12px;height:12px}*::-webkit-scrollbar-track{background:var(--c-scroll-track)}*::-webkit-scrollbar-thumb{background:var(--c-scroll-thumb);border-radius:20px;border:2px solid var(--c-scroll-track)}*::-webkit-scrollbar-thumb:hover{background:var(--c-scroll-thumb-hover)}
|
||||
/* Links */
|
||||
a{color:var(--c-link);text-decoration:none}a:hover{color:var(--c-link-hover)}
|
||||
/* Generic surfaces */
|
||||
.card,.content,.form-card,.item-card,.upload-card{background:var(--c-surface);border:1px solid var(--c-border);box-shadow:var(--c-shadow-sm);color:var(--c-text);border-radius:var(--radius-md)}
|
||||
.item-card:hover{border-color:var(--c-border-strong);box-shadow:var(--c-shadow-md)}
|
||||
.badge{background:var(--c-badge-bg);color:var(--c-badge-text);border:1px solid var(--c-border);font-weight:600;border-radius:var(--radius-sm);padding:.25rem .55rem;font-size:.7rem;letter-spacing:.03em}
|
||||
/* Buttons */
|
||||
.btn,.action-button,.run-backup,.update-system,.actions a.download,.actions a.restore,.upload-actions .primary{background:var(--c-accent);color:#fff !important;border:1px solid var(--c-accent-hover);transition:var(--transition);font-weight:600}
|
||||
.btn:hover,.action-button:hover,.run-backup:hover,.update-system:hover,.actions a.download:hover,.actions a.restore:hover,.upload-actions .primary:hover{background:var(--c-accent-hover)}
|
||||
.btn-danger,.danger-button{background:var(--c-danger);border-color:var(--c-danger-hover)}.btn-danger:hover,.danger-button:hover{background:var(--c-danger-hover)}
|
||||
.ghost{background:transparent;border:1px solid var(--c-border);color:var(--c-text)}.ghost:hover{background:var(--c-surface-alt)}
|
||||
/* Forms */
|
||||
input,select,textarea{background:var(--c-surface-alt);border:1px solid var(--c-border);color:var(--c-text);border-radius:var(--radius-sm);padding:.45rem .65rem}
|
||||
input:focus,select:focus,textarea:focus{border-color:var(--c-accent);box-shadow:var(--c-focus-ring);outline:none}
|
||||
/* Modals */
|
||||
.item-modal{background:var(--c-overlay)}.modal-content{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-lg);box-shadow:var(--c-shadow-lg)}
|
||||
.close-modal{color:var(--c-text-faint)}.close-modal:hover{color:var(--c-text)}
|
||||
/* Status pill */
|
||||
.status-pill{background:var(--c-surface);border:1px solid var(--c-border);color:var(--c-text-soft);padding:.25rem .6rem;border-radius:999px;font-weight:600}
|
||||
.status-pill.on{background:rgba(16,185,129,0.15);border-color:var(--c-success);color:var(--c-success)}
|
||||
.status-pill.off{background:rgba(239,68,68,0.18);border-color:var(--c-danger);color:var(--c-danger)}
|
||||
/* Focus */
|
||||
:focus-visible{outline:none;box-shadow:var(--c-focus-ring)}
|
||||
/* Utility */
|
||||
.text-info{opacity:.85}
|
||||
@media (max-width:768px){.navbar-nav .dropdown-menu{border:none;box-shadow:none;background:rgba(17,24,39,.92);margin-left:15px;margin-top:5px;border-radius:10px}.navbar-nav .dropdown-item{color:#fff}.navbar-nav .dropdown-item:hover,.navbar-nav .dropdown-item:focus{background:rgba(255,255,255,.08);color:#fff;transform:none}.nav-item.dropdown .dropdown-toggle{padding:8px 12px}.dropdown-toggle::after{margin-left:8px}.dropdown-menu-end{background:var(--c-surface);border:1px solid var(--c-border)}.dropdown-menu-end .dropdown-item{color:var(--c-text-soft)}.dropdown-menu-end .dropdown-item:hover{background:var(--c-surface-alt);color:var(--c-text)}}
|
||||
</style>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('default') }}">Inventarsystem</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('default') }}">Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex ms-auto">
|
||||
{% if 'username' in session %}
|
||||
<a id="logout" class="btn btn-outline-danger btn-sm logout-btn" href="{{ url_for('logout') }}" aria-label="Logout">Logout</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<!-- Mobile compatibility scripts -->
|
||||
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
<!--
|
||||
Copyright 2025 Maximilian Gründinger
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 class="text-center">Login</h2>
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">TOTP</label>
|
||||
<div class="password-input-container">
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
<button type="button" class="password-toggle-btn" id="togglePassword" aria-label="Show password">
|
||||
<input type="checkbox" checked="checked">
|
||||
<svg class="eye" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 576 512"><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"></path></svg>
|
||||
<svg class="eye-slash" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 640 512"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const togglePassword = document.getElementById('togglePassword');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const passwordContainer = document.querySelector('.password-input-container');
|
||||
|
||||
togglePassword.addEventListener('click', function() {
|
||||
// Toggle the password field type
|
||||
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||
passwordInput.setAttribute('type', type);
|
||||
|
||||
// Toggle the visual state of the container
|
||||
passwordContainer.classList.toggle('password-visible');
|
||||
|
||||
//Toggle visability of eye according to state
|
||||
|
||||
// Update aria-label for accessibility
|
||||
const isVisible = type === 'text';
|
||||
togglePassword.setAttribute('aria-label', isVisible ? 'Hide password' : 'Show password');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.password-input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
#togglePassword {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#togglePassword {
|
||||
--color: #a5a5b0;
|
||||
--size: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: var(--size);
|
||||
user-select: none;
|
||||
fill: var(--color);
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
#togglePassword .eye-slash {
|
||||
position: absolute;
|
||||
animation: keyframes-fill .5s;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#togglePassword .eye {
|
||||
position: absolute;
|
||||
animation: keyframes-fill .5s;
|
||||
}
|
||||
|
||||
/* ------ On check event ------ */
|
||||
#togglePassword input:checked ~ .eye-slash {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#togglePassword input:checked ~ .eye {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ------ Hide the default checkbox ------ */
|
||||
#togglePassword input {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ------ Animation ------ */
|
||||
@keyframes keyframes-fill {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,2 +1,53 @@
|
||||
<a>This is a Test If this works it would be great!</a>
|
||||
<p>Version: {{ version }}</p>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Keys & HWIDs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">All Keys</h2>
|
||||
<p class="text-muted mb-0">Overview of license keys and bound HWIDs.</p>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('generate_new') }}">
|
||||
<button type="submit" class="btn btn-success">Generate New Key</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">User ID</th>
|
||||
<th scope="col">License Key</th>
|
||||
<th scope="col">HWID</th>
|
||||
<th scope="col" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in keys %}
|
||||
<tr>
|
||||
<td>{{ item.user_id }}</td>
|
||||
<td><code>{{ item.license_key }}</code></td>
|
||||
<td>
|
||||
{% if item.hwid_uuid %}
|
||||
<code>{{ item.hwid_uuid }}</code>
|
||||
{% else %}
|
||||
<span class="text-warning">Not assigned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<form method="POST" action="{{ url_for('remove_key', user_id=item.user_id) }}" onsubmit="return confirm('Remove key for user {{ item.user_id }}?');">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">No keys found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user