This commit is contained in:
Elijah R 2024-04-06 02:26:35 -04:00
commit 67ff874dd3
11 changed files with 622 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
dist/
.parcel-cache/

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock=false

3
Config.ts Normal file
View file

@ -0,0 +1,3 @@
export const Config = {
APIEndpoint: "http://127.0.0.1:5858"
};

2
README.MD Normal file
View file

@ -0,0 +1,2 @@
# CollabVM Authentication Admin Panel
## WIP

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "collabvm-auth-admin-panel",
"version": "1.0.0",
"description": "Admin panel for the CollabVM Authentication Server",
"type": "module",
"scripts": {
"build": "parcel build --no-source-maps --dist-dir dist --public-url '.' src/html/index.html",
"serve": "parcel src/html/index.html",
"clean": "run-script-os",
"clean:darwin:linux": "rm -rf dist .parcel-cache",
"clean:win32": "rd /s /q dist .parcel-cache"
},
"repository": {
"type": "git",
"url": "https://git.computernewb.com/collabvm/collabvm-auth-admin-panel"
},
"author": "Computernewb Development Team",
"license": "GPL-3.0",
"devDependencies": {
"@types/bootstrap": "^5.2.10",
"parcel": "^2.12.0",
"run-script-os": "^1.1.6",
"typescript": "^5.4.4"
},
"dependencies": {
"@hcaptcha/types": "^1.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2"
}
}

3
src/css/style.css Normal file
View file

@ -0,0 +1,3 @@
#adminView, #adminLoginForm, #navbarNav {
display: none;
}

109
src/html/index.html Normal file
View file

@ -0,0 +1,109 @@
<!DOCTYPE HTML>
<html data-bs-theme="dark">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CollabVM Authentication Admin Panel</title>
<link rel="stylesheet" href="../css/style.css"/>
<link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css"/>
<script src="https://kit.fontawesome.com/7add23c1ae.js" crossorigin="anonymous"></script>
<link rel="icon" href="../../assets/favicon.ico"/>
</head>
<body>
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<a class="navbar-brand" href="#">CollabVM Admin Panel</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav" style="display:none!important">
<ul class="navbar-nav me-auto">
</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"></span>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdownMenuLink">
<a class="dropdown-item" href="#" id="accountLogoutButton">Logout</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container-sm" id="loginView">
<div class="row justify-content-center">
<div class="col-6">
<p id="loadingText">Loading...</p>
<form id="adminLoginForm">
<label for="loginUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="loginUsername" name="username" required/>
<label for="loginPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="loginPassword" name="password" required/>
<div id="loginCaptcha"></div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
</div>
<div class="container-lg" id="adminView">
<div id="usersView">
<h1>Users</h1>
<form id="searchUsersForm">
<div class="input-group">
<span class="input-group-text">Filter Username</span>
<input type="text" class="form-control" id="usernameFilter" name="username"/>
<span class="input-group-text">Sort By</span>
<select class="form-select" id="userSortBy" name="sortBy" required>
<option value="id">ID</option>
<option value="username">Username</option>
<option value="email">Email</option>
<option value="date_of_birth">Date of Birth</option>
<option value="cvm_rank">Rank</option>
<option value="banned">Banned</option>
<option value="created">Created At</option>
</select>
<div class="input-group-text">
<input class="form-check-input" type="checkbox" value="" id="userSortDescending">&nbsp;
<label class="form-check-label">Descending?</label>
</div>
<button type="submit" class="btn btn-primary">Search</button>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Rank</th>
<th>Banned</th>
<th>Date of Birth</th>
<th>Created At</th>
<th>Registration IP</th>
</tr>
</thead>
<tbody id="usersTableBody"></tbody>
</table>
<div class="row">
<div class="col-3">
<div class="input-group">
<span class="input-group-text">Page</span>
<input type="number" class="form-control" id="usersPage" name="page" value="1" min="1" max="1" required/>
<span class="input-group-text">of&nbsp;<span id="usersPageCount">1</span></span>
<button type="submit" class="btn btn-primary">Go</button>
</div>
</div>
<div class="col-7"><!-- yeah this is lazy whatever --></div>
<div class="col-2">
<div class="input-group">
<input type="number" class="form-control" id="usersPerPage" name="perPage" value="10" min="1" max="100" required/>
<span class="input-group-text">Per Page</span>
</div>
</div>
</div>
</form>
</div>
</div>
<script src="https://js.hcaptcha.com/1/api.js"></script>
<script type="module" src="../ts/main.ts" type="application/javascript"></script>
</body>
</html>

189
src/ts/AuthManager.ts Normal file
View file

@ -0,0 +1,189 @@
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);
})
}
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);
})
}
listUsers(resultsPerPage : number, page : number, filterUsername : string | undefined, orderBy : string | undefined, orderByDescending : boolean) {
return new Promise<ListUsersResult>(async res => {
if (!this.account) throw new Error("Cannot list users without logging in first");
var data = await fetch(this.apiEndpoint + "/api/v1/admin/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: this.account.sessionToken,
resultsPerPage: resultsPerPage,
page: page,
filterUsername: filterUsername,
orderBy: orderBy,
orderByDescending: orderByDescending
})
});
var json = await data.json() as ListUsersResult;
res(json);
});
}
updateRank(username : string, newRank : number) {
return new Promise<UpdateRankResult>(async res => {
if (!this.account) throw new Error("Cannot update rank without logging in first");
var data = await fetch(this.apiEndpoint + "/api/v1/admin/rank", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: this.account.sessionToken,
username: username,
rank: newRank
})
});
var json = await data.json() as UpdateRankResult;
res(json);
});
}
}
export interface AuthServerInformation {
registrationOpen : boolean;
hcaptcha : {
required : boolean;
siteKey : string | undefined;
};
}
export interface AccountLoginResult {
success : boolean;
token : string | undefined;
error : string | undefined;
verificationRequired : boolean | undefined;
email : string | undefined;
username : string | undefined;
rank : number | undefined;
}
export interface SessionResult {
success : boolean;
error : string | undefined;
banned : boolean;
username : string | undefined;
email : string | undefined;
rank : number | undefined;
}
export interface LogoutResult {
success : boolean;
error : string | undefined;
}
export interface Account {
username : string;
email : string;
sessionToken : string;
}
export interface User {
id : number;
username : string;
email : string;
rank : number;
banned : boolean;
dateOfBirth : string;
dateJoined : string;
registrationIp : string;
}
export interface ListUsersResult {
success : boolean;
error : string | undefined;
totalPageCount : number | undefined;
users : User[] | undefined;
}
export interface UpdateRankResult {
success : boolean;
error : string | undefined;
}

170
src/ts/main.ts Normal file
View file

@ -0,0 +1,170 @@
import * as bootstrap from 'bootstrap';
// trick parcel into including bootstrap js
bootstrap;
import { Config } from '../../Config.js';
import AuthManager from './AuthManager.js';
const elements = {
accountDropdownUsername: document.getElementById('accountDropdownUsername') as HTMLSpanElement,
navbarNav: document.getElementById('navbarNav') as HTMLDivElement,
loginView: document.getElementById('loginView') as HTMLDivElement,
adminView: document.getElementById('adminView') as HTMLDivElement,
loadingText: document.getElementById('loadingText') as HTMLParagraphElement,
adminLoginForm: document.getElementById('adminLoginForm') as HTMLFormElement,
loginUsername: document.getElementById('loginUsername') as HTMLInputElement,
loginPassword: document.getElementById('loginPassword') as HTMLInputElement,
loginCaptcha: document.getElementById('loginCaptcha') as HTMLDivElement,
accountLogoutButton: document.getElementById('accountLogoutButton') as HTMLAnchorElement,
searchUsersForm: document.getElementById('searchUsersForm') as HTMLFormElement,
usernameFilter: document.getElementById('usernameFilter') as HTMLInputElement,
userSortBy: document.getElementById('userSortBy') as HTMLSelectElement,
userSortDescending: document.getElementById('userSortDescending') as HTMLInputElement,
usersPage: document.getElementById('usersPage') as HTMLInputElement,
usersPerPage: document.getElementById('usersPerPage') as HTMLInputElement,
usersPageCount: document.getElementById('usersPageCount') as HTMLSpanElement,
usersTableBody: document.getElementById('usersTableBody') as HTMLTableSectionElement,
};
const RankString = {
1: "User",
2: "Administrator",
3: "Moderator",
};
var auth : AuthManager = new AuthManager(Config.APIEndpoint);
var hcaptchaid : string;
elements.adminLoginForm.addEventListener('submit', async e => {
e.preventDefault();
if (auth.info!.hcaptcha.required) {
var hcaptchaToken = undefined;
if (auth!.info!.hcaptcha.required) {
var response = hcaptcha.getResponse(hcaptchaid);
if (response === "") {
alert("Missing captcha!");
return false;
}
hcaptchaToken = response;
}
var result = await auth.login(elements.loginUsername.value, elements.loginPassword.value, hcaptchaToken);
elements.loginUsername.value = "";
elements.loginPassword.value = "";
hcaptcha.reset(hcaptchaid);
if (result.success) {
if (result.rank !== 2) {
alert("You are not an administrator!");
await auth.logout();
return false;
}
localStorage.setItem(`collabvm_session_${new URL(Config.APIEndpoint).host}`, result.token!);
loadAdminView();
} else {
alert("Login failed: " + result.error);
}
}
return false;
});
elements.accountLogoutButton.addEventListener('click', async () => {
await auth.logout();
localStorage.removeItem(`collabvm_session_${new URL(Config.APIEndpoint).host}`);
loadLoginForm();
});
elements.searchUsersForm.addEventListener('submit', async e => {
e.preventDefault();
var usernameFilter = elements.usernameFilter.value;
var sortBy = elements.userSortBy.value;
var sortDescending = elements.userSortDescending.checked;
var page = parseInt(elements.usersPage.value);
var perPage = parseInt(elements.usersPerPage.value);
var data = await auth.listUsers(perPage, page, usernameFilter, sortBy, sortDescending);
if (!data.success) {
alert("Failed to list users: " + data.error);
return false;
}
elements.usersTableBody.innerHTML = "";
elements.usersPageCount.innerText = data.totalPageCount!.toString(10);
elements.usersPage.max = data.totalPageCount!.toString(10);
for (var user of data.users!) {
var row = elements.usersTableBody.insertRow();
var cell = row.insertCell();
cell.innerText = user.id.toString(10);
cell = row.insertCell();
cell.innerText = user.username;
cell = row.insertCell();
cell.innerText = user.email;
cell = row.insertCell();
// Rank dropdown
(() => {
var _user = user;
var rankSelect = document.createElement('select');
rankSelect.innerHTML = `<option value="1">User</option><option value="2">Administrator</option><option value="3">Moderator</option>`;
rankSelect.value = _user.rank.toString(10);
rankSelect.addEventListener('change', async e => {
var newRank = parseInt(rankSelect.value);
// @ts-ignore
if (!window.confirm(`Are you sure you want to set ${_user.username}'s rank to ${RankString[newRank]}?`)) {
e.preventDefault();
rankSelect.value = _user.rank.toString(10);
return false;
}
var result = await auth.updateRank(_user.username, newRank);
if (!result.success) {
alert("Failed to set rank: " + result.error);
}
});
cell.appendChild(rankSelect);
})();
cell = row.insertCell();
cell.innerText = user.banned ? "Yes" : "No";
cell = row.insertCell();
cell.innerText = user.dateOfBirth;
cell = row.insertCell();
cell.innerText = user.dateJoined;
cell = row.insertCell();
cell.innerText = user.registrationIp;
}
return false;
});
(async () => {
await auth.getAPIInformation();
if (auth!.info!.hcaptcha.required) {
hcaptchaid = hcaptcha.render(elements.loginCaptcha, {
sitekey: auth!.info!.hcaptcha.siteKey!,
});
}
var token = localStorage.getItem(`collabvm_session_${new URL(Config.APIEndpoint).host}`);
if (token) {
var session = await auth.loadSession(token);
if (session.success) {
if (session.rank! !== 2) {
await auth.logout();
localStorage.removeItem(`collabvm_session_${new URL(Config.APIEndpoint).host}`);
loadLoginForm();
}
loadAdminView();
} else {
localStorage.removeItem(`collabvm_session_${new URL(Config.APIEndpoint).host}`);
loadLoginForm();
}
} else loadLoginForm();
})();
function loadAdminView() {
elements.loginView.style.display = "none";
elements.adminView.style.display = "block";
elements.navbarNav.style.display = "";
elements.accountDropdownUsername.innerText = auth!.account!.username;
}
function loadLoginForm() {
elements.loginView.style.display = "block";
elements.adminView.style.display = "none";
elements.navbarNav.setAttribute("style", "display:none!important");
elements.loadingText.style.display = "none";
elements.adminLoginForm.style.display = "block";
}

112
tsconfig.json Normal file
View file

@ -0,0 +1,112 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES2022", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "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": [
"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. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}