mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 14:35:36 +08:00
1. Database Enhancements (db/repositories.go): - Added ConfigRepository interface and implementations for PostgreSQL and SQLite - Updated Repositories struct to include ConfigRepository - Modified NewRepositories to initialize both UserRepo and ConfigRepo 2. FSD Server Improvements: - Removed hardcoded jwtSecret, now retrieved from ConfigRepository (fsd/conn.go, web/auth.go) - Added dynamic welcome message retrieval from ConfigRepository (fsd/conn.go) - Optimized METAR buffer size from 4096 to 512 bytes (fsd/metar.go) - Reduced minimum fields for DeleteATC and DeletePilot packets (fsd/packet.go) - Improved Haversine distance calculation with constants (fsd/postoffice.go) - Added thread-safety documentation for sendError (fsd/client.go) 3. Server Configuration (fsd/server.go): - Added NewDefaultServer to initialize server with environment-based config - Implemented automatic database migration and default admin user creation - Added configurable METAR worker count - Improved logging with slog and environment-based debug level 4. Web Interface Enhancements: - Added user and config editor frontend routes (web/frontend.go, web/routes.go) - Improved JWT handling by retrieving secret from ConfigRepository (web/auth.go) - Enhanced user management API endpoints (web/user.go) - Updated dashboard to display CID and conditional admin links (web/templates/dashboard.html) - Embedded templates using go:embed (web/templates.go) 5. Frontend JavaScript Improvements: - Added networkRatingFromInt helper for readable ratings (web/static/js/openfsd/dashboard.js) - Improved API request handling with auth/no-auth variants (web/static/js/openfsd/api.js) 6. Miscellaneous: - Added sethvargo/go-envconfig dependency for environment variable parsing - Fixed parseVisRange to use 64-bit float parsing (fsd/util.go) - Added strPtr utility function (fsd/util.go, web/main.go) - Improved SVG logo rendering in layout (web/templates/layout.html)
284 lines
15 KiB
HTML
284 lines
15 KiB
HTML
{{ define "title" }}User Editor{{ end }}
|
|
|
|
{{ define "body" }}
|
|
<div class="container mt-4">
|
|
<div class="row">
|
|
<div class="col-12 col-md-4 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Create User</h5>
|
|
<form class="mb-3" id="create-form">
|
|
<div class="mb-3">
|
|
<label for="create-first-name" class="form-label">First Name</label>
|
|
<input type="text" class="form-control" id="create-first-name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="create-last-name" class="form-label">Last Name</label>
|
|
<input type="text" class="form-control" id="create-last-name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="create-password" class="form-label">Password</label>
|
|
<input type="password" class="form-control" id="create-password" minlength="8" required>
|
|
<div class="mt-2">
|
|
<label class="form-label">Password Strength</label>
|
|
<div class="progress" style="height: 10px;">
|
|
<div id="create-password-strength" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
|
</div>
|
|
<small id="create-password-feedback" class="form-text text-muted"></small>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="create-network-rating" class="form-label">Network Rating</label>
|
|
<select class="form-select" id="create-network-rating" required aria-label="Select network rating">
|
|
<option value="-1">Inactive</option>
|
|
<option value="0">Suspended</option>
|
|
<option value="1" selected>Observer</option>
|
|
<option value="2">Student 1</option>
|
|
<option value="3">Student 2</option>
|
|
<option value="4">Student 3</option>
|
|
<option value="5">Controller 1</option>
|
|
<option value="6">Controller 2</option>
|
|
<option value="7">Controller 3</option>
|
|
<option value="8">Instructor 1</option>
|
|
<option value="9">Instructor 2</option>
|
|
<option value="10">Instructor 3</option>
|
|
<option value="11">Supervisor</option>
|
|
<option value="12">Administrator</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Create</button>
|
|
</form>
|
|
<div id="create-success-message" class="alert alert-success d-none" role="alert"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-md-4 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Search for User by CID</h5>
|
|
<form id="search-form">
|
|
<div class="mb-3">
|
|
<label for="search-cid" class="form-label">CID</label>
|
|
<input type="number" class="form-control" id="search-cid" min="1" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Search</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-md-4 mb-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Edit User</h5>
|
|
<form class="mb-3" id="edit-form">
|
|
<div class="mb-3">
|
|
<label for="edit-cid" class="form-label">CID</label>
|
|
<input type="text" class="form-control" id="edit-cid" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="edit-first-name" class="form-label">First Name</label>
|
|
<input type="text" class="form-control" id="edit-first-name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="edit-last-name" class="form-label">Last Name</label>
|
|
<input type="text" class="form-control" id="edit-last-name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="edit-network-rating" class="form-label">Network Rating</label>
|
|
<select class="form-select" id="edit-network-rating" aria-label="Select network rating">
|
|
<option value="-1">Inactive</option>
|
|
<option value="0">Suspended</option>
|
|
<option value="1" selected>Observer</option>
|
|
<option value="2">Student 1</option>
|
|
<option value="3">Student 2</option>
|
|
<option value="4">Student 3</option>
|
|
<option value="5">Controller 1</option>
|
|
<option value="6">Controller 2</option>
|
|
<option value="7">Controller 3</option>
|
|
<option value="8">Instructor 1</option>
|
|
<option value="9">Instructor 2</option>
|
|
<option value="10">Instructor 3</option>
|
|
<option value="11">Supervisor</option>
|
|
<option value="12">Administrator</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="edit-password" class="form-label">New Password (leave blank to keep current)</label>
|
|
<input type="password" class="form-control" id="edit-password" minlength="8">
|
|
<div class="mt-2">
|
|
<label class="form-label">Password Strength</label>
|
|
<div class="progress" style="height: 10px;">
|
|
<div id="edit-password-strength" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
|
</div>
|
|
<small id="edit-password-feedback" class="form-text text-muted"></small>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Update</button>
|
|
</form>
|
|
<div id="edit-success-message" class="alert alert-success d-none" role="alert"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for error messages only -->
|
|
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="messageModalLabel">Error</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="messageModalBody"></div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Function to show modal for errors only
|
|
function showModal(message) {
|
|
const modal = new bootstrap.Modal(document.getElementById('messageModal'));
|
|
const modalBody = document.getElementById('messageModalBody');
|
|
modalBody.textContent = message;
|
|
modal.show();
|
|
}
|
|
|
|
// Form Handlers
|
|
document.getElementById('search-form').addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
const cid = document.getElementById('search-cid').value;
|
|
try {
|
|
const res = await doAPIRequestWithAuth('POST', '/api/v1/user/load', {cid: parseInt(cid)});
|
|
if (res.err) {
|
|
showModal(res.err);
|
|
} else {
|
|
const user = res.data;
|
|
document.getElementById('edit-cid').value = user.cid;
|
|
document.getElementById('edit-first-name').value = user.first_name || '';
|
|
document.getElementById('edit-last-name').value = user.last_name || '';
|
|
document.getElementById('edit-network-rating').value = user.network_rating;
|
|
document.getElementById('edit-password').value = '';
|
|
}
|
|
} catch (xhr) {
|
|
const errMsg = xhr.responseJSON && xhr.responseJSON.err ? xhr.responseJSON.err : 'An error occurred';
|
|
showModal(errMsg);
|
|
console.error('Request failed:', xhr);
|
|
}
|
|
});
|
|
|
|
document.getElementById('create-form').addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
document.getElementById('create-success-message').classList.add('d-none');
|
|
const firstName = document.getElementById('create-first-name').value;
|
|
const lastName = document.getElementById('create-last-name').value;
|
|
const password = document.getElementById('create-password').value;
|
|
const networkRating = document.getElementById('create-network-rating').value;
|
|
const data = {
|
|
password: password,
|
|
network_rating: parseInt(networkRating)
|
|
};
|
|
if (firstName) data.first_name = firstName;
|
|
if (lastName) data.last_name = lastName;
|
|
try {
|
|
const res = await doAPIRequestWithAuth('POST', '/api/v1/user/create', data);
|
|
if (res.err) {
|
|
showModal(res.err);
|
|
} else {
|
|
const successMessage = document.getElementById('create-success-message');
|
|
successMessage.textContent = 'User created successfully. CID: ' + res.data.cid;
|
|
successMessage.classList.remove('d-none');
|
|
document.getElementById('create-first-name').value = '';
|
|
document.getElementById('create-last-name').value = '';
|
|
document.getElementById('create-password').value = '';
|
|
document.getElementById('create-network-rating').value = '-1';
|
|
}
|
|
} catch (xhr) {
|
|
const errMsg = xhr.responseJSON && xhr.responseJSON.err ? xhr.responseJSON.err : 'An error occurred';
|
|
showModal(errMsg);
|
|
console.error('Request failed:', xhr);
|
|
}
|
|
});
|
|
|
|
document.getElementById('edit-form').addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
document.getElementById('edit-success-message').classList.add('d-none');
|
|
const cid = document.getElementById('edit-cid').value;
|
|
const firstName = document.getElementById('edit-first-name').value;
|
|
const lastName = document.getElementById('edit-last-name').value;
|
|
const networkRating = document.getElementById('edit-network-rating').value;
|
|
const password = document.getElementById('edit-password').value;
|
|
const data = {
|
|
cid: parseInt(cid),
|
|
first_name: firstName,
|
|
last_name: lastName,
|
|
network_rating: parseInt(networkRating)
|
|
};
|
|
if (password) data.password = password;
|
|
try {
|
|
const res = await doAPIRequestWithAuth('PATCH', '/api/v1/user/update', data);
|
|
if (res.err) {
|
|
showModal(res.err);
|
|
} else {
|
|
const successMessage = document.getElementById('edit-success-message');
|
|
successMessage.textContent = 'User updated successfully';
|
|
successMessage.classList.remove('d-none');
|
|
}
|
|
} catch (xhr) {
|
|
const errMsg = xhr.responseJSON && xhr.responseJSON.err ? xhr.responseJSON.err : 'An error occurred';
|
|
showModal(errMsg);
|
|
console.error('Request failed:', xhr);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const createPasswordInput = document.getElementById('create-password');
|
|
const createStrengthBar = document.getElementById('create-password-strength');
|
|
const createFeedback = document.getElementById('create-password-feedback');
|
|
const editPasswordInput = document.getElementById('edit-password');
|
|
const editStrengthBar = document.getElementById('edit-password-strength');
|
|
const editFeedback = document.getElementById('edit-password-feedback');
|
|
|
|
const evaluatePassword = (password, strengthBar, feedback) => {
|
|
if (!password) {
|
|
strengthBar.style.width = '0%';
|
|
strengthBar.className = 'progress-bar';
|
|
feedback.textContent = '';
|
|
return;
|
|
}
|
|
|
|
let strength = 0;
|
|
if (password.length >= 8) strength += 50;
|
|
if (/[A-Z]/.test(password)) strength += 15;
|
|
if (/[a-z]/.test(password)) strength += 15;
|
|
if (/[0-9]/.test(password)) strength += 10;
|
|
if (/[^A-Za-z0-9]/.test(password)) strength += 10;
|
|
|
|
strength = Math.min(strength, 100);
|
|
strengthBar.style.width = `${strength}%`;
|
|
|
|
if (strength < 60) {
|
|
strengthBar.className = 'progress-bar bg-danger';
|
|
feedback.textContent = 'Weak: Include uppercase, lowercase, numbers, or symbols.';
|
|
} else if (strength < 80) {
|
|
strengthBar.className = 'progress-bar bg-warning';
|
|
feedback.textContent = 'Moderate: Add more character types for strength.';
|
|
} else {
|
|
strengthBar.className = 'progress-bar bg-success';
|
|
feedback.textContent = 'Strong: Good password!';
|
|
}
|
|
};
|
|
|
|
createPasswordInput.addEventListener('input', () => {
|
|
evaluatePassword(createPasswordInput.value, createStrengthBar, createFeedback);
|
|
});
|
|
|
|
editPasswordInput.addEventListener('input', () => {
|
|
evaluatePassword(editPasswordInput.value, editStrengthBar, editFeedback);
|
|
});
|
|
});
|
|
</script>
|
|
{{ end }} |