mirror of
https://github.com/renorris/openfsd
synced 2026-04-20 09:55:43 +08:00
v0.1.0-alpha
Changes:
- Implement bootstrapping library for managing several concurrent internal services
- Refactor concurrency model for connections/logical clients and their associated I/O
- Refactor server context singleton
- Refactor error handling
- Most errors are now gracefully sent to the FSD client directly encoded as an $ER packet,
enhancing visibility and debugging
- Most errors are now rightfully treated as non-fatal
- Refactor package/dependency graph
- Refactor calling conventions/interfaces for many packages
- Refactor database package
- Refactor post office
Features:
- Add VATSIM-esque HTTP/JSON "data feed"
- Add ephemeral in-memory database option
- Add user management REST API
- Add improved web interface
- Add MySQL support (drop SQLite support)
This commit is contained in:
219
web/static/js/admin_dashboard.js
Normal file
219
web/static/js/admin_dashboard.js
Normal file
@@ -0,0 +1,219 @@
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||
}
|
||||
|
||||
const showResponse = (msg) => {
|
||||
$('#responseBody').text(msg);
|
||||
const toast = new bootstrap.Toast($('#responseToast'));
|
||||
toast.show();
|
||||
};
|
||||
|
||||
$(document).ready(() => {
|
||||
const token = getCookie("token")
|
||||
|
||||
const userFetchModal = $("#user-fetch-modal")
|
||||
const userEditModal = $("#user-edit-modal")
|
||||
const userCreateModal = $("#user-create-modal")
|
||||
|
||||
const userCreateStatusMsg = $("#user-create-statusmsg")
|
||||
|
||||
const userEditCID = $("#user-edit-cid-header")
|
||||
const userEditEmail = $("#user-edit-email")
|
||||
const userEditFirstname = $("#user-edit-firstname")
|
||||
const userEditLastname = $("#user-edit-lastname")
|
||||
const userEditNetworkRating = $("#user-edit-networkrating")
|
||||
const userEditPilotRating = $("#user-edit-pilotrating")
|
||||
const userEditLastUpdated = $("#user-edit-lastupdated")
|
||||
const userEditCreatedAt = $("#user-edit-createdat")
|
||||
|
||||
const userEditPassword = $("#user-edit-password")
|
||||
const userEditFSDPassword = $("#user-edit-fsdpassword")
|
||||
|
||||
const userEditUpdateButton = $("#user-edit-updatebutton")
|
||||
const userEditDeleteButton = $("#user-edit-deletebutton")
|
||||
|
||||
const userCreateEmail = $("#user-create-email")
|
||||
const userCreateFirstname = $("#user-create-firstname")
|
||||
const userCreateLastname = $("#user-create-lastname")
|
||||
const userCreateNetworkRating = $("#user-create-networkrating")
|
||||
const userCreatePilotRating = $("#user-create-pilotrating")
|
||||
const userCreatePassword = $("#user-create-password")
|
||||
const userCreateFSDPassword = $("#user-create-fsdpassword")
|
||||
|
||||
const userCreateSubmitButton = $("#user-create-submitbutton")
|
||||
|
||||
const userFetchButton = $("#user-fetch-button")
|
||||
const userCreateButton = $("#user-create-button")
|
||||
|
||||
const userFetchCID = $("#user-fetch-cid")
|
||||
const userFetchInitiate = $("#user-fetch-initiate")
|
||||
|
||||
const toastClipboardButton = $("#toast-clipboard-button")
|
||||
|
||||
userFetchButton.click(() => {
|
||||
let modal = new bootstrap.Modal(userFetchModal)
|
||||
modal.show()
|
||||
})
|
||||
|
||||
userCreateButton.click(() => {
|
||||
let modal = new bootstrap.Modal(userCreateModal)
|
||||
modal.show()
|
||||
})
|
||||
|
||||
userFetchInitiate.click(() => {
|
||||
new bootstrap.Modal(userFetchModal).hide()
|
||||
|
||||
if (userFetchCID.val() === "") {
|
||||
showResponse("Error: CID empty")
|
||||
return
|
||||
}
|
||||
|
||||
new bootstrap.Modal(userFetchModal).hide()
|
||||
|
||||
// Fetch users
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: `/api/v1/users/${parseInt(userFetchCID.val())}`,
|
||||
dataType: "json",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
userEditCID.text(data.user.cid)
|
||||
userEditEmail.val(data.user.email)
|
||||
userEditFirstname.val(data.user.first_name)
|
||||
userEditLastname.val(data.user.last_name)
|
||||
userEditNetworkRating.val(data.user.network_rating)
|
||||
userEditNetworkRating.change()
|
||||
userEditPilotRating.val(data.user.pilot_rating)
|
||||
userEditPilotRating.change()
|
||||
userEditLastUpdated.text(data.user.updated_at)
|
||||
userEditCreatedAt.text(data.user.created_at)
|
||||
new bootstrap.Modal(userEditModal).show()
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
showResponse('Error: ' + jqXHR.statusText + '\nMessage: ' + JSON.parse(jqXHR.responseText).msg)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
userEditUpdateButton.click(() => {
|
||||
|
||||
if (!window.confirm(`Are you sure you want to update CID ${userEditCID.text()}?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update user
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: `/api/v1/users`,
|
||||
contentType: "application/json",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
data: JSON.stringify({
|
||||
cid: parseInt(userEditCID.text()),
|
||||
user: {
|
||||
cid: parseInt(userEditCID.text()),
|
||||
email: userEditEmail.val(),
|
||||
first_name: userEditFirstname.val(),
|
||||
last_name: userEditLastname.val(),
|
||||
network_rating: parseInt(userEditNetworkRating.val()),
|
||||
pilot_rating: parseInt(userEditPilotRating.val()),
|
||||
password: userEditPassword.val(),
|
||||
fsd_password: userEditFSDPassword.val(),
|
||||
}
|
||||
}),
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
showResponse(`Successfully updated user ${userEditCID.text()}`)
|
||||
new bootstrap.Modal(userEditModal).hide()
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
showResponse('Error: ' + jqXHR.statusText + '\nMessage: ' + JSON.parse(jqXHR.responseText).msg)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
userEditDeleteButton.click(() => {
|
||||
|
||||
if (!window.confirm(`Are you sure you want to delete CID ${userEditCID.text()}?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete user
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: `/api/v1/users`,
|
||||
contentType: "application/json",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
data: JSON.stringify({
|
||||
cid: parseInt(userEditCID.text()),
|
||||
}),
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
showResponse(`Successfully deleted user ${userEditCID.text()}`)
|
||||
new bootstrap.Modal(userEditModal).hide()
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
showResponse('Error: ' + jqXHR.statusText + '\nMessage: ' + JSON.parse(jqXHR.responseText).msg)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
userCreateSubmitButton.click(() => {
|
||||
|
||||
if (userCreatePassword === "" || userCreateFSDPassword === "") {
|
||||
showResponse("error: password is empty")
|
||||
return
|
||||
}
|
||||
|
||||
// Create user
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/api/v1/users`,
|
||||
contentType: "application/json",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
data: JSON.stringify({
|
||||
user: {
|
||||
email: userCreateEmail.val(),
|
||||
first_name: userCreateFirstname.val(),
|
||||
last_name: userCreateLastname.val(),
|
||||
network_rating: parseInt(userCreateNetworkRating.val()),
|
||||
pilot_rating: parseInt(userCreatePilotRating.val()),
|
||||
password: userCreatePassword.val(),
|
||||
fsd_password: userCreateFSDPassword.val(),
|
||||
}
|
||||
}),
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
|
||||
let pwdMsg = userCreatePassword.val() === "" ? `\nPassword: ${data.user.password}` : ``
|
||||
userCreateFSDPassword.val() === "" ? pwdMsg += `\nFSD Password: ${data.user.fsd_password}` : ``
|
||||
|
||||
showResponse(`Successfully created user: CID = ${data.user.cid}\n${pwdMsg}`)
|
||||
new bootstrap.Modal(userCreateModal).hide()
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
showResponse('Error: ' + jqXHR.statusText + '\nMessage: ' + JSON.parse(jqXHR.responseText).msg)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
toastClipboardButton.click(() => {
|
||||
let text = $("#responseBody").text()
|
||||
navigator.clipboard.writeText(text).then(r => {
|
||||
$("#toast-copy-banner").text("Copied!")
|
||||
setTimeout(() => {
|
||||
$("#toast-copy-banner").text("")
|
||||
}, 2000)
|
||||
});
|
||||
})
|
||||
})
|
||||
13
web/static/js/alert.js
Normal file
13
web/static/js/alert.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function setAlert(id, message, color) {
|
||||
const html = `
|
||||
<div class="alert alert-${color} alert-dismissible fade show" role="alert">
|
||||
<span>${message}</span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>`
|
||||
|
||||
$('#' + id).html(html)
|
||||
}
|
||||
|
||||
function clearAlert(id) {
|
||||
$('#' + id).html('')
|
||||
}
|
||||
7
web/static/js/bootstrap.min.js
vendored
Normal file
7
web/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
89
web/static/js/dashboard.js
Normal file
89
web/static/js/dashboard.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const showResponse = (msg) => {
|
||||
$('#responseBody').text(msg);
|
||||
const toast = new bootstrap.Toast($('#responseToast'));
|
||||
toast.show();
|
||||
};
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#change-primary-password-form').on('submit', function (event) {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
|
||||
// Get the values from the input fields
|
||||
const oldPassword = $('input[name="old-password-primary"]').val();
|
||||
const newPassword = $('input[name="new-password-primary"]').val();
|
||||
const newPasswordConfirm = $('input[name="new-password-confirm-primary"]').val();
|
||||
|
||||
if (oldPassword === "" || newPassword === "" || newPasswordConfirm === "") {
|
||||
showResponse('Error: password empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if new password and old password are equal
|
||||
if (newPassword !== newPasswordConfirm) {
|
||||
showResponse('Error: passwords do not match');
|
||||
return; // Exit the function if they are not equal
|
||||
}
|
||||
|
||||
// Send AJAX POST request
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/changepassword',
|
||||
data: {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
change_fsd_password: false
|
||||
},
|
||||
xhrFields: {
|
||||
withCredentials: true
|
||||
},
|
||||
}).done(function (data, textStatus, jqXHR) {
|
||||
$('input[name="old-password-primary"]').val("")
|
||||
$('input[name="new-password-primary"]').val("")
|
||||
$('input[name="new-password-confirm-primary"]').val("")
|
||||
showResponse('Password changed successfully')
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
showResponse('Error: ' + jqXHR.statusText + '\nResponse: ' + jqXHR.responseText); // Alert with the status code and response body
|
||||
})
|
||||
})
|
||||
|
||||
$('#change-fsd-password-form').on('submit', function (event) {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
|
||||
// Get the values from the input fields
|
||||
const newPassword = $('input[name="new-password-fsd"]').val();
|
||||
const newPasswordConfirm = $('input[name="new-password-confirm-fsd"]').val();
|
||||
|
||||
if (newPassword === "" || newPasswordConfirm === "") {
|
||||
showResponse('Error: password empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if new password and old password are equal
|
||||
if (newPassword !== newPasswordConfirm) {
|
||||
showResponse('Error: passwords do not match');
|
||||
return; // Exit the function if they are not equal
|
||||
}
|
||||
|
||||
// Send AJAX POST request
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/changepassword',
|
||||
data: {
|
||||
new_password: newPassword,
|
||||
change_fsd_password: true
|
||||
},
|
||||
xhrFields: {
|
||||
withCredentials: true // Include credentials (cookies, etc.) in the request
|
||||
},
|
||||
}).done(function (data, textStatus, jqXHR) {
|
||||
console.log('done')
|
||||
$('input[name="new-password-fsd"]').val("")
|
||||
$('input[name="new-password-confirm-fsd"]').val("")
|
||||
showResponse('FSD password changed successfully!')
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
showResponse('Error: ' + jqXHR.statusText + '\nResponse: ' + jqXHR.responseText); // Alert with the status code and response body
|
||||
})
|
||||
})
|
||||
});
|
||||
2
web/static/js/jquery-3.7.1.min.js
vendored
Normal file
2
web/static/js/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
32
web/static/js/login.js
Normal file
32
web/static/js/login.js
Normal file
@@ -0,0 +1,32 @@
|
||||
$(document).ready(function () {
|
||||
$('#login-form').on('submit', function (event) {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
|
||||
// Get the values from the input fields
|
||||
const cid = $('input[name="cid"]').val();
|
||||
const password = $('input[name="password"]').val();
|
||||
|
||||
// Send AJAX POST request
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/login',
|
||||
data: {
|
||||
cid: cid,
|
||||
password: password
|
||||
},
|
||||
xhrFields: {
|
||||
withCredentials: true // Include credentials (cookies, etc.) in the request
|
||||
},
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
// If we received a successful response from the server (HTTP 200)
|
||||
if (jqXHR.status === 204) {
|
||||
window.location.href = '/dashboard'; // Redirect to dashboard
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
// If we received an error response from the server
|
||||
alert('Error: ' + jqXHR.statusText + '\nMessage: ' + jqXHR.responseText); // Alert with the status code and response body
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
13
web/static/js/logout.js
Normal file
13
web/static/js/logout.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function deleteAllCookies() {
|
||||
document.cookie.split(';').forEach(cookie => {
|
||||
const eqPos = cookie.indexOf('=');
|
||||
const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie;
|
||||
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
deleteAllCookies()
|
||||
localStorage.clear()
|
||||
window.location.assign('/login')
|
||||
}
|
||||
9
web/static/js/modal.js
Normal file
9
web/static/js/modal.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function showModal(id) {
|
||||
const m = new bootstrap.Modal(document.getElementById(id))
|
||||
m.show()
|
||||
}
|
||||
|
||||
function hideModal(id) {
|
||||
const m = new bootstrap.Modal(document.getElementById(id))
|
||||
m.hide()
|
||||
}
|
||||
6
web/static/js/popper.min.js
vendored
Normal file
6
web/static/js/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
0
web/static/js/user-api.js
Normal file
0
web/static/js/user-api.js
Normal file
27
web/static/js/verifytotp.js
Normal file
27
web/static/js/verifytotp.js
Normal file
@@ -0,0 +1,27 @@
|
||||
async function handleVerifyTOTPSubmit(ev) {
|
||||
ev.preventDefault()
|
||||
|
||||
const totp = $('#totp').val()
|
||||
|
||||
const res = await fetch("/api/v1/auth/verifytotp", {
|
||||
body: JSON.stringify({
|
||||
code: parseInt(totp),
|
||||
}),
|
||||
cache: "no-cache",
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
const msg = res.status === 401 ? 'incorrect 2FA code' : `HTTP ${res.status} ${res.statusText}`
|
||||
setAlert('alert', `Error: ${msg}`, 'danger')
|
||||
return
|
||||
}
|
||||
|
||||
const resPayload = await res.json()
|
||||
|
||||
localStorage.setItem("token", resPayload.token)
|
||||
window.location.replace('/dashboard')
|
||||
}
|
||||
|
||||
$('#form').submit(handleVerifyTOTPSubmit)
|
||||
Reference in New Issue
Block a user