implement account authentication (webapp)
This commit is contained in:
parent
999bdd0809
commit
54255cc118
|
|
@ -10,5 +10,9 @@ export const Config = {
|
|||
"wss://computernewb.com/collab-vm/vm6",
|
||||
"wss://computernewb.com/collab-vm/vm7",
|
||||
"wss://computernewb.com/collab-vm/vm8",
|
||||
]
|
||||
],
|
||||
Auth: {
|
||||
Enabled: false,
|
||||
APIEndpoint: "http://127.0.0.1:5858"
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,14 @@ tr.user-moderator > td, .chat-username-moderator, .username-moderator {
|
|||
color: #00FF00 !important;
|
||||
}
|
||||
|
||||
tr.user-unregistered > td, .chat-username-unregistered, .username-unregistered {
|
||||
color: #b1b1b1 !important;
|
||||
}
|
||||
|
||||
tr.user-registered > td, .chat-username-registered, .username-registered {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
tr.user-turn > td {
|
||||
background-color: #cfe2ff !important;
|
||||
--bs-table-bg-state: #cfe2ff !important;
|
||||
|
|
@ -290,3 +298,7 @@ div[data-cvm-node=vm0b0t] {
|
|||
filter:blur(40px)!important;
|
||||
}
|
||||
}
|
||||
|
||||
#accountDropdownMenuLink, #accountModalError {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -97,15 +97,74 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="hcaptchaModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content bg-dark text-light">
|
||||
<div class="modal-body">
|
||||
<div id="captcha-box"></div>
|
||||
<div class="modal fade" id="accountModal" tabindex="-1" aria-labelledby="accountModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content bg-dark text-light">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="accountModalTitle">Login</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 alert-dismissible" id="accountModalError" role="alert">
|
||||
<span id="accountModalErrorText"></span>
|
||||
<button type="button" class="btn-close" aria-label="Close" id="accountModalErrorDismiss"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountLoginSection">
|
||||
<form id="accountLoginForm">
|
||||
<label for="accountLoginUsername">Username</label><br/>
|
||||
<input id="accountLoginUsername" type="text" class="form-control bg-dark text-light" placeholder="Username" name="username" required/><br>
|
||||
<label for="accountLoginPassword">Password</label><br/>
|
||||
<input id="accountLoginPassword" type="password" class="form-control bg-dark text-light" placeholder="Password" name="password" required><br>
|
||||
<div id="accountLoginCaptcha"></div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="accountRegisterSection">
|
||||
<form id="accountRegisterForm">
|
||||
<label for="accountRegisterEmail">E-Mail</label><br/>
|
||||
<input id="accountRegisterEmail" type="email" class="form-control bg-dark text-light" placeholder="E-Mail" name="email" required/><br>
|
||||
<label for="accountRegisterUsername">Username</label><br/>
|
||||
<input id="accountRegisterUsername" type="text" class="form-control bg-dark text-light" placeholder="Username" name="username" required/><br>
|
||||
<label for="accountRegisterPassword">Password</label><br/>
|
||||
<input id="accountRegisterPassword" type="password" class="form-control bg-dark text-light" placeholder="Password" name="password" required><br>
|
||||
<label for="accountRegisterConfirmPassword">Confirm Password</label><br/>
|
||||
<input id="accountRegisterConfirmPassword" type="password" class="form-control bg-dark text-light" placeholder="Confirm Password" name="confirmpassword" required><br>
|
||||
<div id="accountRegisterCaptcha"></div>
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="accountVerifyEmailSection">
|
||||
<center>
|
||||
<i class="fa-solid fa-envelope" style="font-size: 12rem"></i>
|
||||
<p id="accountVerifyEmailText"></p>
|
||||
<form id="accountVerifyEmailForm">
|
||||
<label for="accountVerifyEmailCode">Code</label><br>
|
||||
<input id="accountVerifyEmailCode" type="text" class="form-control bg-dark text-light" name="code" required><br>
|
||||
<label for="accountVerifyEmailPassword">Your password</label><br>
|
||||
<input id="accountVerifyEmailPassword" type="password" class="form-control bg-dark text-light" placeholder="Password" name="password" required/><br/>
|
||||
<button type="submit" class="btn btn-primary">Verify</button>
|
||||
</form>
|
||||
</center>
|
||||
</div>
|
||||
<div id="accountSettingsSection">
|
||||
<form id="accountSettingsForm">
|
||||
<label for="accountSettingsEmail">E-Mail</label><br>
|
||||
<input id="accountSettingsEmail" type="email" class="form-control bg-dark text-light" placeholder="E-Mail" name="email" required/><br/>
|
||||
<label for="accountSettingsUsername">Username</label><br>
|
||||
<input id="accountSettingsUsername" type="text" class="form-control bg-dark text-light" placeholder="Username" name="username" required/><br/>
|
||||
<label for="accountSettingsNewPassword">New Password</label>
|
||||
<input id="accountSettingsNewPassword" type="password" class="form-control bg-dark text-light" placeholder="New Password" name="password"/><br/>
|
||||
<label for="accountSettingsConfirmNewPassword">Confirm New Password</label>
|
||||
<input id="accountSettingsConfirmNewPassword" type="password" class="form-control bg-dark text-light" placeholder="Confirm New Password" name="confirmpassword"/><br/>
|
||||
<label for="accountSettingsCurrentPassword">Current Password</label>
|
||||
<input id="accountSettingsCurrentPassword" type="password" class="form-control bg-dark text-light" placeholder="Current Password" name="currentpassword" required/><br/>
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#"><span id="siteNameText"></span></a>
|
||||
|
|
@ -113,7 +172,7 @@
|
|||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a id="homeBtn" href="#" class="nav-link active" aria-current="page"><i class="fa-solid fa-house"></i> <span id="homeBtnText"></span></a>
|
||||
</li>
|
||||
|
|
@ -136,6 +195,17 @@
|
|||
<a href="https://computernewb.com/collab-vm/user-vm" class="nav-link"><i class="fa-solid fa-user"></i> UserVM</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-text dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="accountDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-user"></i> <span id="accountDropdownUsername">Not Logged in</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end bg-dark text-light" aria-labelledby="accountDropdownMenuLink">
|
||||
<a class="dropdown-item bg-dark text-light" href="#" id="accountLoginButton">Log in</a>
|
||||
<a class="dropdown-item bg-dark text-light" href="#" id="accountRegisterButton">Register</a>
|
||||
<a class="dropdown-item bg-dark text-light" href="#" id="accountSettingsButton">Account Settings</a>
|
||||
<a class="dropdown-item bg-dark text-light" href="#" id="accountLogoutButton">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -204,7 +274,7 @@
|
|||
</table>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-dark text-light" id="username">Username</span>
|
||||
<span class="input-group-text bg-dark username-unregistered" id="username">Username</span>
|
||||
<input type="text" class="form-control bg-dark text-light" id="chat-input"/>
|
||||
<div class="input-group-text bg-dark text-light" id="xssCheckboxContainer">
|
||||
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox"/>
|
||||
|
|
@ -215,7 +285,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
<script src="https://js.hcaptcha.com/1/api.js"></script>
|
||||
<script type="module" src="../ts/main.ts" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
211
src/ts/AuthManager.ts
Normal file
211
src/ts/AuthManager.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
export default class AuthManager {
|
||||
apiEndpoint : string;
|
||||
info : AuthServerInformation | null;
|
||||
account : Account | null;
|
||||
constructor(apiEndpoint : string) {
|
||||
this.apiEndpoint = apiEndpoint;
|
||||
this.info = null;
|
||||
this.account = null;
|
||||
}
|
||||
|
||||
getAPIInformation() : Promise<AuthServerInformation> {
|
||||
return new Promise(async res => {
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/info");
|
||||
this.info = await data.json();
|
||||
res(this.info!);
|
||||
})
|
||||
}
|
||||
|
||||
login(username : string, password : string, captchaToken : string | undefined) : Promise<AccountLoginResult> {
|
||||
return new Promise(async (res,rej) => {
|
||||
if (!this.info) throw new Error("Cannot login before fetching API information.");
|
||||
if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
captchaToken: captchaToken
|
||||
})
|
||||
});
|
||||
var json = await data.json() as AccountLoginResult;
|
||||
if (!json) throw new Error("data.json() gave null or undefined result");
|
||||
if (json.success && !json.verificationRequired) {
|
||||
this.account = {
|
||||
username: json.username!,
|
||||
email: json.email!,
|
||||
sessionToken: json.token!
|
||||
}
|
||||
}
|
||||
res(json);
|
||||
})
|
||||
}
|
||||
|
||||
loadSession(token : string) {
|
||||
return new Promise<SessionResult>(async (res, rej) => {
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
})
|
||||
});
|
||||
var json = await data.json() as SessionResult;
|
||||
if (json.success) {
|
||||
this.account = {
|
||||
sessionToken: token,
|
||||
username: json.username!,
|
||||
email: json.email!,
|
||||
};
|
||||
}
|
||||
res(json);
|
||||
})
|
||||
}
|
||||
|
||||
register(username : string, password : string, email : string, captchaToken : string | undefined) : Promise<AccountRegisterResult> {
|
||||
return new Promise(async (res, rej) => {
|
||||
if (!this.info) throw new Error("Cannot login before fetching API information.");
|
||||
if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
email: email,
|
||||
captchatoken: captchaToken
|
||||
})
|
||||
});
|
||||
res(await data.json() as AccountRegisterResult);
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
return new Promise<LogoutResult>(async res => {
|
||||
if (!this.account) throw new Error("Cannot log out without logging in first");
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: this.account.sessionToken
|
||||
})
|
||||
});
|
||||
var json = await data.json() as LogoutResult;
|
||||
this.account = null;
|
||||
res(json);
|
||||
})
|
||||
}
|
||||
|
||||
verifyEmail(username : string, password : string, code : string) {
|
||||
return new Promise<VerifyEmailResult>(async res => {
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
code: code,
|
||||
})
|
||||
});
|
||||
res(await data.json() as VerifyEmailResult);
|
||||
});
|
||||
}
|
||||
|
||||
updateAccount(currentPassword : string, newEmail : string | undefined, newUsername : string | undefined, newPassword : string | undefined) {
|
||||
return new Promise<UpdateAccountResult>(async res => {
|
||||
if (!this.account) throw new Error("Cannot update account without being logged in.");
|
||||
if (!newEmail && !newUsername && !newPassword) throw new Error("Cannot update account without any new information.");
|
||||
var data = await fetch(this.apiEndpoint + "/api/v1/update", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: this.account!.sessionToken,
|
||||
currentPassword: currentPassword,
|
||||
newPassword: newPassword,
|
||||
username: newUsername,
|
||||
email: newEmail,
|
||||
})
|
||||
});
|
||||
var json = await data.json() as UpdateAccountResult;
|
||||
if (json.success) {
|
||||
if (this.account!.email !== newEmail) this.account!.email = newEmail!;
|
||||
if (this.account!.username !== newUsername) this.account!.username = newUsername!;
|
||||
if (json.sessionExpired || json.verificationRequired) {
|
||||
this.account = null;
|
||||
}
|
||||
}
|
||||
res(json);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthServerInformation {
|
||||
registrationOpen : boolean;
|
||||
hcaptcha : {
|
||||
required : boolean;
|
||||
siteKey : string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccountRegisterResult {
|
||||
success : boolean;
|
||||
error : string | undefined;
|
||||
verificationRequired : boolean | undefined;
|
||||
username : string | undefined;
|
||||
email : string | undefined;
|
||||
sessionToken : string | undefined;
|
||||
}
|
||||
|
||||
export interface AccountLoginResult {
|
||||
success : boolean;
|
||||
token : string | undefined;
|
||||
error : string | undefined;
|
||||
verificationRequired : boolean | undefined;
|
||||
email : string | undefined;
|
||||
username : string | undefined;
|
||||
}
|
||||
|
||||
export interface SessionResult {
|
||||
success : boolean;
|
||||
error : string | undefined;
|
||||
banned : boolean;
|
||||
username : string | undefined;
|
||||
email : string | undefined;
|
||||
}
|
||||
|
||||
export interface VerifyEmailResult {
|
||||
success : boolean;
|
||||
error : string | undefined;
|
||||
sessionToken : string | undefined;
|
||||
}
|
||||
|
||||
export interface LogoutResult {
|
||||
success : boolean;
|
||||
error : string | undefined;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
username : string;
|
||||
email : string;
|
||||
sessionToken : string;
|
||||
}
|
||||
|
||||
export interface UpdateAccountResult {
|
||||
success : boolean;
|
||||
error : string | undefined;
|
||||
verificationRequired : boolean | undefined;
|
||||
sessionExpired : boolean | undefined;
|
||||
}
|
||||
|
|
@ -43,6 +43,18 @@ export enum I18nStringKey {
|
|||
kError_UsernameTaken = 'kError_UsernameTaken',
|
||||
kError_UsernameInvalid = 'kError_UsernameInvalid',
|
||||
kError_UsernameBlacklisted = 'kError_UsernameBlacklisted',
|
||||
|
||||
// Auth
|
||||
kAccountModal_Login = 'kAccountModal_Login',
|
||||
kAccountModal_Register = 'kAccountModal_Register',
|
||||
kAccountModal_Verify = 'kAccountModal_Verify',
|
||||
kAccountModal_AccountSettings = 'kAccountModal_AccountSettings',
|
||||
|
||||
kAccountModal_VerifyText = 'kAccountModal_VerifyText',
|
||||
kMissingCaptcha = 'kMissingCaptcha',
|
||||
kPasswordsMustMatch = 'kPasswordsMustMatch',
|
||||
|
||||
kNotLoggedIn = 'kNotLoggedIn',
|
||||
}
|
||||
|
||||
// This models the JSON structure.
|
||||
|
|
|
|||
310
src/ts/main.ts
310
src/ts/main.ts
|
|
@ -13,6 +13,7 @@ import MuteState from './protocol/MuteState.js';
|
|||
import { Unsubscribe } from 'nanoevents';
|
||||
import { I18nStringKey, TheI18n } from './i18n.js';
|
||||
import { Format } from './format.js';
|
||||
import AuthManager from './AuthManager.js';
|
||||
|
||||
// Elements
|
||||
const w = window as any;
|
||||
|
|
@ -65,9 +66,49 @@ const elements = {
|
|||
indefTurnBtn: document.getElementById('indefTurnBtn') as HTMLButtonElement,
|
||||
qemuMonitorInput: document.getElementById('qemuMonitorInput') as HTMLInputElement,
|
||||
qemuMonitorSendBtn: document.getElementById('qemuMonitorSendBtn') as HTMLButtonElement,
|
||||
qemuMonitorOutput: document.getElementById('qemuMonitorOutput') as HTMLTextAreaElement
|
||||
qemuMonitorOutput: document.getElementById('qemuMonitorOutput') as HTMLTextAreaElement,
|
||||
// Auth
|
||||
accountDropdownUsername: document.getElementById("accountDropdownUsername") as HTMLSpanElement,
|
||||
accountDropdownMenuLink: document.getElementById("accountDropdownMenuLink") as HTMLDivElement,
|
||||
accountLoginButton: document.getElementById("accountLoginButton") as HTMLAnchorElement,
|
||||
accountRegisterButton: document.getElementById("accountRegisterButton") as HTMLAnchorElement,
|
||||
accountSettingsButton: document.getElementById("accountSettingsButton") as HTMLAnchorElement,
|
||||
accountLogoutButton: document.getElementById("accountLogoutButton") as HTMLAnchorElement,
|
||||
accountModal: document.getElementById("accountModal") as HTMLDivElement,
|
||||
accountModalError: document.getElementById("accountModalError") as HTMLDivElement,
|
||||
accountModalErrorText: document.getElementById("accountModalErrorText") as HTMLSpanElement,
|
||||
accountModalErrorDismiss: document.getElementById("accountModalErrorDismiss") as HTMLButtonElement,
|
||||
accountLoginSection: document.getElementById("accountLoginSection") as HTMLDivElement,
|
||||
accountRegisterSection: document.getElementById("accountRegisterSection") as HTMLDivElement,
|
||||
accountVerifyEmailSection: document.getElementById("accountVerifyEmailSection") as HTMLDivElement,
|
||||
accountVerifyEmailText: document.getElementById("accountVerifyEmailText") as HTMLParagraphElement,
|
||||
accountModalTitle: document.getElementById("accountModalTitle") as HTMLHeadingElement,
|
||||
accountLoginForm: document.getElementById("accountLoginForm") as HTMLFormElement,
|
||||
accountRegisterForm: document.getElementById("accountRegisterForm") as HTMLFormElement,
|
||||
accountVerifyEmailForm: document.getElementById("accountVerifyEmailForm") as HTMLFormElement,
|
||||
accountLoginCaptcha: document.getElementById("accountLoginCaptcha") as HTMLDivElement,
|
||||
accountRegisterCaptcha: document.getElementById("accountRegisterCaptcha") as HTMLDivElement,
|
||||
|
||||
accountLoginUsername: document.getElementById("accountLoginUsername") as HTMLInputElement,
|
||||
accountLoginPassword: document.getElementById("accountLoginPassword") as HTMLInputElement,
|
||||
accountRegisterEmail: document.getElementById("accountRegisterEmail") as HTMLInputElement,
|
||||
accountRegisterUsername: document.getElementById("accountRegisterUsername") as HTMLInputElement,
|
||||
accountRegisterPassword: document.getElementById("accountRegisterPassword") as HTMLInputElement,
|
||||
accountRegisterConfirmPassword: document.getElementById("accountRegisterConfirmPassword") as HTMLInputElement,
|
||||
accountVerifyEmailCode: document.getElementById("accountVerifyEmailCode") as HTMLInputElement,
|
||||
accountVerifyEmailPassword: document.getElementById("accountVerifyEmailPassword") as HTMLInputElement,
|
||||
|
||||
accountSettingsSection: document.getElementById("accountSettingsSection") as HTMLDivElement,
|
||||
accountSettingsForm: document.getElementById("accountSettingsForm") as HTMLFormElement,
|
||||
accountSettingsEmail: document.getElementById("accountSettingsEmail") as HTMLInputElement,
|
||||
accountSettingsUsername: document.getElementById("accountSettingsUsername") as HTMLInputElement,
|
||||
accountSettingsNewPassword: document.getElementById("accountSettingsNewPassword") as HTMLInputElement,
|
||||
accountSettingsConfirmNewPassword: document.getElementById("accountSettingsConfirmNewPassword") as HTMLInputElement,
|
||||
accountSettingsCurrentPassword: document.getElementById("accountSettingsCurrentPassword") as HTMLInputElement,
|
||||
};
|
||||
|
||||
let auth : AuthManager|null = null;
|
||||
|
||||
/* Start OSK */
|
||||
let commonKeyboardOptions = {
|
||||
onKeyPress: (button: string) => onKeyPress(button),
|
||||
|
|
@ -353,12 +394,23 @@ async function openVM(vm: VM): Promise<void> {
|
|||
closeVM();
|
||||
});
|
||||
|
||||
// auth
|
||||
VM!.on('auth', async server => {
|
||||
elements.changeUsernameBtn.style.display = "none";
|
||||
if (Config.Auth.Enabled && Config.Auth.APIEndpoint === server && auth!.account) {
|
||||
VM!.loginAccount(auth!.account.sessionToken);
|
||||
} else if (!Config.Auth.Enabled || Config.Auth.APIEndpoint !== server) {
|
||||
auth = new AuthManager(server);
|
||||
await renderAuth();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the client to open
|
||||
await VM!.WaitForOpen();
|
||||
|
||||
// Connect to node
|
||||
chatMessage('', `<b>${vm.id}</b><hr>`);
|
||||
let username = localStorage.getItem('username');
|
||||
let username = Config.Auth.Enabled ? null : localStorage.getItem('username');
|
||||
let connected = await VM.connect(vm.id, username);
|
||||
elements.adminInputVMID.value = vm.id;
|
||||
w.VMName = vm.id;
|
||||
|
|
@ -411,8 +463,18 @@ function closeVM() {
|
|||
elements.voteYesLabel.innerText = '0';
|
||||
elements.voteNoLabel.innerText = '0';
|
||||
elements.xssCheckbox.checked = false;
|
||||
elements.username.classList.remove('username-admin', 'username-moderator');
|
||||
elements.username.classList.add('text-light');
|
||||
elements.username.classList.remove('username-admin', 'username-moderator', 'username-registered');
|
||||
elements.username.classList.add('username-unregistered');
|
||||
// Reset rename button
|
||||
elements.changeUsernameBtn.style.display = "inline-block";
|
||||
// Reset auth if it was changed by the VM
|
||||
if (Config.Auth.Enabled && auth?.apiEndpoint !== Config.Auth.APIEndpoint) {
|
||||
auth = new AuthManager(Config.Auth.APIEndpoint);
|
||||
renderAuth();
|
||||
} else if (auth && !Config.Auth.Enabled) {
|
||||
auth = null;
|
||||
elements.accountDropdownMenuLink.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
|
|
@ -472,6 +534,10 @@ function chatMessage(username: string, message: string) {
|
|||
userclass = 'chat-username-unregistered';
|
||||
msgclass = 'chat-unregistered';
|
||||
break;
|
||||
case Rank.Registered:
|
||||
userclass = 'chat-username-registered';
|
||||
msgclass = 'chat-registered';
|
||||
break;
|
||||
case Rank.Admin:
|
||||
userclass = 'chat-username-admin';
|
||||
msgclass = 'chat-admin';
|
||||
|
|
@ -510,6 +576,9 @@ function addUser(user: User) {
|
|||
case Rank.Moderator:
|
||||
tr.classList.add('user-moderator');
|
||||
break;
|
||||
case Rank.Registered:
|
||||
tr.classList.add('user-registered');
|
||||
break;
|
||||
case Rank.Unregistered:
|
||||
tr.classList.add('user-unregistered');
|
||||
break;
|
||||
|
|
@ -517,7 +586,7 @@ function addUser(user: User) {
|
|||
if (user.username === w.username) tr.classList.add('user-current');
|
||||
tr.appendChild(td);
|
||||
let u = { user: user, element: tr };
|
||||
if (rank !== Rank.Unregistered) userModOptions(u);
|
||||
if (rank === Rank.Admin || rank === Rank.Moderator) userModOptions(u);
|
||||
elements.userlist.appendChild(tr);
|
||||
if (olduser !== undefined) olduser.element = tr;
|
||||
else users.push(u);
|
||||
|
|
@ -581,6 +650,7 @@ function turnUpdate(status: TurnStatus) {
|
|||
}
|
||||
if (turn === -1) elements.turnstatus.innerText = '';
|
||||
else {
|
||||
//@ts-ignore
|
||||
turnInterval = setInterval(() => turnIntervalCb(), 1000);
|
||||
setTurnStatus();
|
||||
}
|
||||
|
|
@ -593,6 +663,7 @@ function voteUpdate(status: VoteStatus) {
|
|||
elements.voteYesLabel.innerText = status.yesVotes.toString();
|
||||
elements.voteNoLabel.innerText = status.noVotes.toString();
|
||||
voteTimer = Math.floor(status.timeToEnd / 1000);
|
||||
//@ts-ignore
|
||||
voteInterval = setInterval(() => updateVoteEndTime(), 1000);
|
||||
updateVoteEndTime();
|
||||
}
|
||||
|
|
@ -670,6 +741,7 @@ let usernameClick = false;
|
|||
const loginModal = new bootstrap.Modal(elements.loginModal);
|
||||
elements.loginModal.addEventListener('shown.bs.modal', () => elements.adminPassword.focus());
|
||||
elements.username.addEventListener('click', () => {
|
||||
if (auth) return;
|
||||
if (!usernameClick) {
|
||||
usernameClick = true;
|
||||
setInterval(() => (usernameClick = false), 1000);
|
||||
|
|
@ -702,6 +774,7 @@ function onLogin(_rank: Rank, _perms: Permissions) {
|
|||
elements.username.classList.remove('text-dark', 'text-light');
|
||||
if (rank === Rank.Admin) elements.username.classList.add('username-admin');
|
||||
if (rank === Rank.Moderator) elements.username.classList.add('username-moderator');
|
||||
if (rank === Rank.Registered) elements.username.classList.add('username-registered');
|
||||
elements.staffbtns.style.display = 'block';
|
||||
if (_perms.restore) elements.restoreBtn.style.display = 'inline-block';
|
||||
if (_perms.reboot) elements.rebootBtn.style.display = 'inline-block';
|
||||
|
|
@ -716,7 +789,8 @@ function onLogin(_rank: Rank, _perms: Permissions) {
|
|||
}
|
||||
if (_perms.xss) elements.xssCheckboxContainer.style.display = 'inline-block';
|
||||
if (_perms.forcevote) elements.forceVotePanel.style.display = 'block';
|
||||
for (const user of users) userModOptions(user);
|
||||
if (rank !== Rank.Registered)
|
||||
for (const user of users) userModOptions(user);
|
||||
}
|
||||
|
||||
function userModOptions(user: { user: User; element: HTMLTableRowElement }) {
|
||||
|
|
@ -788,6 +862,224 @@ elements.qemuMonitorSendBtn.addEventListener('click', () => sendQEMUCommand());
|
|||
elements.qemuMonitorInput.addEventListener('keypress', (e) => e.key === 'Enter' && sendQEMUCommand());
|
||||
|
||||
elements.osk.addEventListener('click', () => elements.oskContainer.classList.toggle('d-none'));
|
||||
// Auth stuff
|
||||
async function renderAuth() {
|
||||
if (auth === null) throw new Error("Cannot renderAuth when auth is null.");
|
||||
await auth.getAPIInformation();
|
||||
elements.accountDropdownUsername.innerText = TheI18n.GetString(I18nStringKey.kNotLoggedIn);
|
||||
elements.accountDropdownMenuLink.style.display = "block";
|
||||
if (!auth!.info!.registrationOpen)
|
||||
elements.accountRegisterButton.style.display = "none";
|
||||
else
|
||||
elements.accountRegisterButton.style.display = "block";
|
||||
elements.accountLoginButton.style.display = "block";
|
||||
elements.accountSettingsButton.style.display = "none";
|
||||
elements.accountLogoutButton.style.display = "none";
|
||||
elements.accountRegisterCaptcha.innerHTML = "";
|
||||
elements.accountLoginCaptcha.innerHTML = "";
|
||||
if (auth!.info!.hcaptcha.required) {
|
||||
var hconfig = {sitekey: auth!.info!.hcaptcha.siteKey!};
|
||||
hcaptcha.render(elements.accountRegisterCaptcha, hconfig);
|
||||
hcaptcha.render(elements.accountLoginCaptcha, hconfig);
|
||||
}
|
||||
var token = localStorage.getItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
|
||||
if (token) {
|
||||
var result = await auth!.loadSession(token);
|
||||
if (result.success) {
|
||||
loadAccount();
|
||||
} else {
|
||||
localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
|
||||
}
|
||||
}
|
||||
}
|
||||
function loadAccount() {
|
||||
if (auth === null || auth.account === null) throw new Error("Cannot loadAccount when auth or auth.account is null.");
|
||||
elements.accountDropdownUsername.innerText = auth!.account!.username;
|
||||
elements.accountLoginButton.style.display = "none";
|
||||
elements.accountRegisterButton.style.display = "none";
|
||||
elements.accountSettingsButton.style.display = "block";
|
||||
elements.accountLogoutButton.style.display = "block";
|
||||
if (VM) VM.loginAccount(auth.account.sessionToken);
|
||||
}
|
||||
const accountModal = new bootstrap.Modal(elements.accountModal);
|
||||
elements.accountModalErrorDismiss.addEventListener('click', () => elements.accountModalError.style.display = "none");
|
||||
elements.accountLoginButton.addEventListener("click", () => {
|
||||
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_Login);
|
||||
elements.accountRegisterSection.style.display = "none";
|
||||
elements.accountVerifyEmailSection.style.display = "none";
|
||||
elements.accountLoginSection.style.display = "block";
|
||||
elements.accountSettingsSection.style.display = "none";
|
||||
accountModal.show();
|
||||
});
|
||||
elements.accountRegisterButton.addEventListener("click", () => {
|
||||
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_Register);
|
||||
elements.accountRegisterSection.style.display = "block";
|
||||
elements.accountVerifyEmailSection.style.display = "none";
|
||||
elements.accountLoginSection.style.display = "none";
|
||||
elements.accountSettingsSection.style.display = "none";
|
||||
accountModal.show();
|
||||
});
|
||||
elements.accountSettingsButton.addEventListener("click", () => {
|
||||
elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_AccountSettings);
|
||||
elements.accountRegisterSection.style.display = "none";
|
||||
elements.accountVerifyEmailSection.style.display = "none";
|
||||
elements.accountLoginSection.style.display = "none";
|
||||
elements.accountSettingsSection.style.display = "block";
|
||||
// Fill fields
|
||||
elements.accountSettingsUsername.value = auth!.account!.username;
|
||||
elements.accountSettingsEmail.value = auth!.account!.email;
|
||||
accountModal.show();
|
||||
});
|
||||
elements.accountLogoutButton.addEventListener('click', async () => {
|
||||
if (!auth?.account) return;
|
||||
await auth.logout();
|
||||
localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
|
||||
if (VM) closeVM();
|
||||
renderAuth();
|
||||
});
|
||||
// i dont know if theres a better place to put this
|
||||
let accountBeingVerified;
|
||||
elements.accountLoginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
var hcaptchaToken = undefined;
|
||||
var hcaptchaID = undefined;
|
||||
if (auth!.info!.hcaptcha.required) {
|
||||
hcaptchaID = elements.accountLoginCaptcha.children[0].getAttribute("data-hcaptcha-widget-id")!
|
||||
var response = hcaptcha.getResponse(hcaptchaID);
|
||||
if (response === "") {
|
||||
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha);
|
||||
elements.accountModalError.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
hcaptchaToken = response;
|
||||
}
|
||||
var username = elements.accountLoginUsername.value;
|
||||
var password = elements.accountLoginPassword.value;
|
||||
var result = await auth!.login(username, password, hcaptchaToken);
|
||||
if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID);
|
||||
if (result.success) {
|
||||
elements.accountLoginUsername.value = "";
|
||||
elements.accountLoginPassword.value = "";
|
||||
if (result.verificationRequired) {
|
||||
accountBeingVerified = result.username;
|
||||
elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, result.email!);
|
||||
elements.accountLoginSection.style.display = "none";
|
||||
elements.accountVerifyEmailSection.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.token!);
|
||||
loadAccount();
|
||||
accountModal.hide();
|
||||
} else {
|
||||
elements.accountModalErrorText.innerHTML = result.error!;
|
||||
elements.accountModalError.style.display = "block";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
elements.accountRegisterForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
var hcaptchaToken = undefined;
|
||||
var hcaptchaID = undefined;
|
||||
if (auth!.info!.hcaptcha.required) {
|
||||
hcaptchaID = elements.accountRegisterCaptcha.children[0].getAttribute("data-hcaptcha-widget-id")!
|
||||
var response = hcaptcha.getResponse(hcaptchaID);
|
||||
if (response === "") {
|
||||
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha);
|
||||
elements.accountModalError.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
hcaptchaToken = response;
|
||||
}
|
||||
var username = elements.accountRegisterUsername.value;
|
||||
var password = elements.accountRegisterPassword.value;
|
||||
var email = elements.accountRegisterEmail.value;
|
||||
if (password !== elements.accountRegisterConfirmPassword.value) {
|
||||
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch);
|
||||
elements.accountModalError.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
var result = await auth!.register(username, password, email, hcaptchaToken);
|
||||
if (auth!.info!.hcaptcha.required) hcaptcha.reset(hcaptchaID);
|
||||
if (result.success) {
|
||||
elements.accountRegisterUsername.value = "";
|
||||
elements.accountRegisterEmail.value = "";
|
||||
elements.accountRegisterPassword.value = "";
|
||||
elements.accountRegisterConfirmPassword.value = "";
|
||||
if (result.verificationRequired) {
|
||||
accountBeingVerified = result.username;
|
||||
elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, result.email!);
|
||||
elements.accountRegisterSection.style.display = "none";
|
||||
elements.accountVerifyEmailSection.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.sessionToken!);
|
||||
await auth!.loadSession(result.sessionToken!);
|
||||
loadAccount();
|
||||
accountModal.hide();
|
||||
} else {
|
||||
elements.accountModalErrorText.innerHTML = result.error!;
|
||||
elements.accountModalError.style.display = "block";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
elements.accountVerifyEmailForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
var username = accountBeingVerified!;
|
||||
var code = elements.accountVerifyEmailCode.value;
|
||||
var password = elements.accountVerifyEmailPassword.value;
|
||||
var result = await auth!.verifyEmail(username, password, code);
|
||||
if (result.success) {
|
||||
elements.accountVerifyEmailCode.value = "";
|
||||
elements.accountVerifyEmailPassword.value = "";
|
||||
localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.sessionToken!);
|
||||
await auth!.loadSession(result.sessionToken!);
|
||||
loadAccount();
|
||||
accountModal.hide();
|
||||
} else {
|
||||
elements.accountModalErrorText.innerHTML = result.error!;
|
||||
elements.accountModalError.style.display = "block";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
elements.accountSettingsForm.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
var oldUsername = auth!.account!.username;
|
||||
var oldEmail = auth!.account!.email;
|
||||
var username = elements.accountSettingsUsername.value === auth!.account!.username ? undefined : elements.accountSettingsUsername.value;
|
||||
var email = elements.accountSettingsEmail.value === auth!.account!.email ? undefined : elements.accountSettingsEmail.value;
|
||||
var password = elements.accountSettingsNewPassword.value === "" ? undefined : elements.accountSettingsNewPassword.value;
|
||||
var currentPassword = elements.accountSettingsCurrentPassword.value;
|
||||
if (password && password !== elements.accountSettingsConfirmNewPassword.value) {
|
||||
elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch);
|
||||
elements.accountModalError.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
var result = await auth!.updateAccount(currentPassword, email, username, password);
|
||||
if (result.success) {
|
||||
elements.accountSettingsNewPassword.value = "";
|
||||
elements.accountSettingsConfirmNewPassword.value = "";
|
||||
elements.accountSettingsCurrentPassword.value = "";
|
||||
if (result.verificationRequired) {
|
||||
renderAuth();
|
||||
accountBeingVerified = username ?? oldUsername;
|
||||
elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, email ?? oldEmail);
|
||||
elements.accountSettingsSection.style.display = "none";
|
||||
elements.accountVerifyEmailSection.style.display = "block";
|
||||
return false;
|
||||
} else if (result.sessionExpired) {
|
||||
accountModal.hide();
|
||||
localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host);
|
||||
if (VM) closeVM();
|
||||
renderAuth();
|
||||
} else {
|
||||
accountModal.hide();
|
||||
}
|
||||
} else {
|
||||
elements.accountModalErrorText.innerHTML = result.error!;
|
||||
elements.accountModalError.style.display = "block";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Public API
|
||||
w.collabvm = {
|
||||
|
|
@ -838,6 +1130,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
// Initalize the i18n system
|
||||
await TheI18n.Init();
|
||||
|
||||
// Initialize authentication if enabled
|
||||
if (Config.Auth.Enabled) {
|
||||
auth = new AuthManager(Config.Auth.APIEndpoint);
|
||||
renderAuth();
|
||||
}
|
||||
|
||||
document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM);
|
||||
|
||||
// Load all VMs
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ export interface CollabVMClientEvents {
|
|||
|
||||
badpw: () => void;
|
||||
login: (rank: Rank, perms: Permissions) => void;
|
||||
|
||||
// Auth stuff
|
||||
auth: (server: string) => void;
|
||||
accountlogin: (success: boolean) => void;
|
||||
}
|
||||
|
||||
// types for private emitter
|
||||
|
|
@ -58,6 +62,7 @@ export default class CollabVMClient {
|
|||
private perms: Permissions = new Permissions(0);
|
||||
private voteStatus: VoteStatus | null = null;
|
||||
private node: string | null = null;
|
||||
private auth: boolean = false;
|
||||
// events that are used internally and not exposed
|
||||
private internalEmitter: Emitter<CollabVMClientPrivateEvents>;
|
||||
// public events
|
||||
|
|
@ -341,6 +346,20 @@ export default class CollabVMClient {
|
|||
break;
|
||||
}
|
||||
}
|
||||
// auth stuff
|
||||
case 'auth': {
|
||||
this.publicEmitter.emit('auth', msgArr[1]);
|
||||
this.auth = true;
|
||||
break;
|
||||
}
|
||||
case 'login': {
|
||||
if (msgArr[1] === "1") {
|
||||
this.rank = Rank.Registered;
|
||||
this.publicEmitter.emit('login', Rank.Registered, new Permissions(0));
|
||||
}
|
||||
this.publicEmitter.emit('accountlogin', msgArr[1] === "1");
|
||||
break;
|
||||
}
|
||||
case 'admin': {
|
||||
switch (msgArr[1]) {
|
||||
case '0': {
|
||||
|
|
@ -592,6 +611,15 @@ export default class CollabVMClient {
|
|||
this.send('admin', AdminOpcode.HideScreen, hidden ? '1' : '0');
|
||||
}
|
||||
|
||||
// Login to account
|
||||
loginAccount(token: string) {
|
||||
this.send('login', token);
|
||||
}
|
||||
|
||||
usesAccountAuth() {
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
private onInternal<E extends keyof CollabVMClientPrivateEvents>(event: E, callback: CollabVMClientPrivateEvents[E]): Unsubscribe {
|
||||
return this.internalEmitter.on(event, callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export class Permissions {
|
|||
|
||||
export enum Rank {
|
||||
Unregistered = 0,
|
||||
Registered = 1,
|
||||
Admin = 2,
|
||||
Moderator = 3
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@
|
|||
"kError_UnexpectedDisconnection": "You have been disconnected from the server.",
|
||||
"kError_UsernameTaken": "That username is already taken",
|
||||
"kError_UsernameInvalid": "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.",
|
||||
"kError_UsernameBlacklisted": "That username has been blacklisted."
|
||||
"kError_UsernameBlacklisted": "That username has been blacklisted.",
|
||||
|
||||
"kAccountModal_Login": "Login",
|
||||
"kAccountModal_Register": "Register",
|
||||
"kAccountModal_Verify": "Verify E-Mail",
|
||||
"kAccountModal_AccountSettings": "Account Settings",
|
||||
|
||||
"kMissingCaptcha": "Please fill out the captcha.",
|
||||
"kPasswordsMustMatch": "Passwords must match.",
|
||||
"kAccountModal_VerifyText": "We sent an E-Mail to {0}. To verify your account, please enter the 8-digit code from the E-Mail below.",
|
||||
|
||||
"kNotLoggedIn": "Not Logged in"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"typeRoots": [
|
||||
"node_modules/@hcaptcha"
|
||||
], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user