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:
Reese Norris
2024-10-07 12:50:39 -07:00
parent de94e668f0
commit 57d54d6705
138 changed files with 8279 additions and 4095 deletions

View 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
View 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

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

32
web/static/js/login.js Normal file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

View File

View 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)