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/vm6",
|
||||||
"wss://computernewb.com/collab-vm/vm7",
|
"wss://computernewb.com/collab-vm/vm7",
|
||||||
"wss://computernewb.com/collab-vm/vm8",
|
"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;
|
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 {
|
tr.user-turn > td {
|
||||||
background-color: #cfe2ff !important;
|
background-color: #cfe2ff !important;
|
||||||
--bs-table-bg-state: #cfe2ff !important;
|
--bs-table-bg-state: #cfe2ff !important;
|
||||||
|
|
@ -290,3 +298,7 @@ div[data-cvm-node=vm0b0t] {
|
||||||
filter:blur(40px)!important;
|
filter:blur(40px)!important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#accountDropdownMenuLink, #accountModalError {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
@ -97,15 +97,74 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="hcaptchaModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="accountModal" tabindex="-1" aria-labelledby="accountModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content bg-dark text-light">
|
<div class="modal-content bg-dark text-light">
|
||||||
<div class="modal-body">
|
<div class="modal-header">
|
||||||
<div id="captcha-box"></div>
|
<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 id="accountLoginSection">
|
||||||
</div>
|
<form id="accountLoginForm">
|
||||||
</div>
|
<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">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#"><span id="siteNameText"></span></a>
|
<a class="navbar-brand" href="#"><span id="siteNameText"></span></a>
|
||||||
|
|
@ -113,7 +172,7 @@
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
<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>
|
<a id="homeBtn" href="#" class="nav-link active" aria-current="page"><i class="fa-solid fa-house"></i> <span id="homeBtnText"></span></a>
|
||||||
</li>
|
</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>
|
<a href="https://computernewb.com/collab-vm/user-vm" class="nav-link"><i class="fa-solid fa-user"></i> UserVM</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -204,7 +274,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<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"/>
|
<input type="text" class="form-control bg-dark text-light" id="chat-input"/>
|
||||||
<div class="input-group-text bg-dark text-light" id="xssCheckboxContainer">
|
<div class="input-group-text bg-dark text-light" id="xssCheckboxContainer">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox"/>
|
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox"/>
|
||||||
|
|
@ -215,7 +285,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<script type="module" src="../ts/main.ts" type="application/javascript"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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_UsernameTaken = 'kError_UsernameTaken',
|
||||||
kError_UsernameInvalid = 'kError_UsernameInvalid',
|
kError_UsernameInvalid = 'kError_UsernameInvalid',
|
||||||
kError_UsernameBlacklisted = 'kError_UsernameBlacklisted',
|
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.
|
// 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 { Unsubscribe } from 'nanoevents';
|
||||||
import { I18nStringKey, TheI18n } from './i18n.js';
|
import { I18nStringKey, TheI18n } from './i18n.js';
|
||||||
import { Format } from './format.js';
|
import { Format } from './format.js';
|
||||||
|
import AuthManager from './AuthManager.js';
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
|
|
@ -65,9 +66,49 @@ const elements = {
|
||||||
indefTurnBtn: document.getElementById('indefTurnBtn') as HTMLButtonElement,
|
indefTurnBtn: document.getElementById('indefTurnBtn') as HTMLButtonElement,
|
||||||
qemuMonitorInput: document.getElementById('qemuMonitorInput') as HTMLInputElement,
|
qemuMonitorInput: document.getElementById('qemuMonitorInput') as HTMLInputElement,
|
||||||
qemuMonitorSendBtn: document.getElementById('qemuMonitorSendBtn') as HTMLButtonElement,
|
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 */
|
/* Start OSK */
|
||||||
let commonKeyboardOptions = {
|
let commonKeyboardOptions = {
|
||||||
onKeyPress: (button: string) => onKeyPress(button),
|
onKeyPress: (button: string) => onKeyPress(button),
|
||||||
|
|
@ -353,12 +394,23 @@ async function openVM(vm: VM): Promise<void> {
|
||||||
closeVM();
|
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
|
// Wait for the client to open
|
||||||
await VM!.WaitForOpen();
|
await VM!.WaitForOpen();
|
||||||
|
|
||||||
// Connect to node
|
// Connect to node
|
||||||
chatMessage('', `<b>${vm.id}</b><hr>`);
|
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);
|
let connected = await VM.connect(vm.id, username);
|
||||||
elements.adminInputVMID.value = vm.id;
|
elements.adminInputVMID.value = vm.id;
|
||||||
w.VMName = vm.id;
|
w.VMName = vm.id;
|
||||||
|
|
@ -411,8 +463,18 @@ function closeVM() {
|
||||||
elements.voteYesLabel.innerText = '0';
|
elements.voteYesLabel.innerText = '0';
|
||||||
elements.voteNoLabel.innerText = '0';
|
elements.voteNoLabel.innerText = '0';
|
||||||
elements.xssCheckbox.checked = false;
|
elements.xssCheckbox.checked = false;
|
||||||
elements.username.classList.remove('username-admin', 'username-moderator');
|
elements.username.classList.remove('username-admin', 'username-moderator', 'username-registered');
|
||||||
elements.username.classList.add('text-light');
|
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() {
|
async function loadList() {
|
||||||
|
|
@ -472,6 +534,10 @@ function chatMessage(username: string, message: string) {
|
||||||
userclass = 'chat-username-unregistered';
|
userclass = 'chat-username-unregistered';
|
||||||
msgclass = 'chat-unregistered';
|
msgclass = 'chat-unregistered';
|
||||||
break;
|
break;
|
||||||
|
case Rank.Registered:
|
||||||
|
userclass = 'chat-username-registered';
|
||||||
|
msgclass = 'chat-registered';
|
||||||
|
break;
|
||||||
case Rank.Admin:
|
case Rank.Admin:
|
||||||
userclass = 'chat-username-admin';
|
userclass = 'chat-username-admin';
|
||||||
msgclass = 'chat-admin';
|
msgclass = 'chat-admin';
|
||||||
|
|
@ -510,6 +576,9 @@ function addUser(user: User) {
|
||||||
case Rank.Moderator:
|
case Rank.Moderator:
|
||||||
tr.classList.add('user-moderator');
|
tr.classList.add('user-moderator');
|
||||||
break;
|
break;
|
||||||
|
case Rank.Registered:
|
||||||
|
tr.classList.add('user-registered');
|
||||||
|
break;
|
||||||
case Rank.Unregistered:
|
case Rank.Unregistered:
|
||||||
tr.classList.add('user-unregistered');
|
tr.classList.add('user-unregistered');
|
||||||
break;
|
break;
|
||||||
|
|
@ -517,7 +586,7 @@ function addUser(user: User) {
|
||||||
if (user.username === w.username) tr.classList.add('user-current');
|
if (user.username === w.username) tr.classList.add('user-current');
|
||||||
tr.appendChild(td);
|
tr.appendChild(td);
|
||||||
let u = { user: user, element: tr };
|
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);
|
elements.userlist.appendChild(tr);
|
||||||
if (olduser !== undefined) olduser.element = tr;
|
if (olduser !== undefined) olduser.element = tr;
|
||||||
else users.push(u);
|
else users.push(u);
|
||||||
|
|
@ -581,6 +650,7 @@ function turnUpdate(status: TurnStatus) {
|
||||||
}
|
}
|
||||||
if (turn === -1) elements.turnstatus.innerText = '';
|
if (turn === -1) elements.turnstatus.innerText = '';
|
||||||
else {
|
else {
|
||||||
|
//@ts-ignore
|
||||||
turnInterval = setInterval(() => turnIntervalCb(), 1000);
|
turnInterval = setInterval(() => turnIntervalCb(), 1000);
|
||||||
setTurnStatus();
|
setTurnStatus();
|
||||||
}
|
}
|
||||||
|
|
@ -593,6 +663,7 @@ function voteUpdate(status: VoteStatus) {
|
||||||
elements.voteYesLabel.innerText = status.yesVotes.toString();
|
elements.voteYesLabel.innerText = status.yesVotes.toString();
|
||||||
elements.voteNoLabel.innerText = status.noVotes.toString();
|
elements.voteNoLabel.innerText = status.noVotes.toString();
|
||||||
voteTimer = Math.floor(status.timeToEnd / 1000);
|
voteTimer = Math.floor(status.timeToEnd / 1000);
|
||||||
|
//@ts-ignore
|
||||||
voteInterval = setInterval(() => updateVoteEndTime(), 1000);
|
voteInterval = setInterval(() => updateVoteEndTime(), 1000);
|
||||||
updateVoteEndTime();
|
updateVoteEndTime();
|
||||||
}
|
}
|
||||||
|
|
@ -670,6 +741,7 @@ let usernameClick = false;
|
||||||
const loginModal = new bootstrap.Modal(elements.loginModal);
|
const loginModal = new bootstrap.Modal(elements.loginModal);
|
||||||
elements.loginModal.addEventListener('shown.bs.modal', () => elements.adminPassword.focus());
|
elements.loginModal.addEventListener('shown.bs.modal', () => elements.adminPassword.focus());
|
||||||
elements.username.addEventListener('click', () => {
|
elements.username.addEventListener('click', () => {
|
||||||
|
if (auth) return;
|
||||||
if (!usernameClick) {
|
if (!usernameClick) {
|
||||||
usernameClick = true;
|
usernameClick = true;
|
||||||
setInterval(() => (usernameClick = false), 1000);
|
setInterval(() => (usernameClick = false), 1000);
|
||||||
|
|
@ -702,6 +774,7 @@ function onLogin(_rank: Rank, _perms: Permissions) {
|
||||||
elements.username.classList.remove('text-dark', 'text-light');
|
elements.username.classList.remove('text-dark', 'text-light');
|
||||||
if (rank === Rank.Admin) elements.username.classList.add('username-admin');
|
if (rank === Rank.Admin) elements.username.classList.add('username-admin');
|
||||||
if (rank === Rank.Moderator) elements.username.classList.add('username-moderator');
|
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';
|
elements.staffbtns.style.display = 'block';
|
||||||
if (_perms.restore) elements.restoreBtn.style.display = 'inline-block';
|
if (_perms.restore) elements.restoreBtn.style.display = 'inline-block';
|
||||||
if (_perms.reboot) elements.rebootBtn.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.xss) elements.xssCheckboxContainer.style.display = 'inline-block';
|
||||||
if (_perms.forcevote) elements.forceVotePanel.style.display = '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 }) {
|
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.qemuMonitorInput.addEventListener('keypress', (e) => e.key === 'Enter' && sendQEMUCommand());
|
||||||
|
|
||||||
elements.osk.addEventListener('click', () => elements.oskContainer.classList.toggle('d-none'));
|
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
|
// Public API
|
||||||
w.collabvm = {
|
w.collabvm = {
|
||||||
|
|
@ -838,6 +1130,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
// Initalize the i18n system
|
// Initalize the i18n system
|
||||||
await TheI18n.Init();
|
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);
|
document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM);
|
||||||
|
|
||||||
// Load all VMs
|
// Load all VMs
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ export interface CollabVMClientEvents {
|
||||||
|
|
||||||
badpw: () => void;
|
badpw: () => void;
|
||||||
login: (rank: Rank, perms: Permissions) => void;
|
login: (rank: Rank, perms: Permissions) => void;
|
||||||
|
|
||||||
|
// Auth stuff
|
||||||
|
auth: (server: string) => void;
|
||||||
|
accountlogin: (success: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// types for private emitter
|
// types for private emitter
|
||||||
|
|
@ -58,6 +62,7 @@ export default class CollabVMClient {
|
||||||
private perms: Permissions = new Permissions(0);
|
private perms: Permissions = new Permissions(0);
|
||||||
private voteStatus: VoteStatus | null = null;
|
private voteStatus: VoteStatus | null = null;
|
||||||
private node: string | null = null;
|
private node: string | null = null;
|
||||||
|
private auth: boolean = false;
|
||||||
// events that are used internally and not exposed
|
// events that are used internally and not exposed
|
||||||
private internalEmitter: Emitter<CollabVMClientPrivateEvents>;
|
private internalEmitter: Emitter<CollabVMClientPrivateEvents>;
|
||||||
// public events
|
// public events
|
||||||
|
|
@ -341,6 +346,20 @@ export default class CollabVMClient {
|
||||||
break;
|
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': {
|
case 'admin': {
|
||||||
switch (msgArr[1]) {
|
switch (msgArr[1]) {
|
||||||
case '0': {
|
case '0': {
|
||||||
|
|
@ -592,6 +611,15 @@ export default class CollabVMClient {
|
||||||
this.send('admin', AdminOpcode.HideScreen, hidden ? '1' : '0');
|
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 {
|
private onInternal<E extends keyof CollabVMClientPrivateEvents>(event: E, callback: CollabVMClientPrivateEvents[E]): Unsubscribe {
|
||||||
return this.internalEmitter.on(event, callback);
|
return this.internalEmitter.on(event, callback);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export class Permissions {
|
||||||
|
|
||||||
export enum Rank {
|
export enum Rank {
|
||||||
Unregistered = 0,
|
Unregistered = 0,
|
||||||
|
Registered = 1,
|
||||||
Admin = 2,
|
Admin = 2,
|
||||||
Moderator = 3
|
Moderator = 3
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,17 @@
|
||||||
"kError_UnexpectedDisconnection": "You have been disconnected from the server.",
|
"kError_UnexpectedDisconnection": "You have been disconnected from the server.",
|
||||||
"kError_UsernameTaken": "That username is already taken",
|
"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_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. */
|
// "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. */
|
// "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. */
|
// "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. */
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user