UI work
/ build (push) Successful in 2m3s Details
/ build_x64 (push) Successful in 3m14s Details
/ build_arm64 (push) Successful in 3m19s Details
/ build_win64 (push) Successful in 17m8s Details

This commit is contained in:
core 2023-10-10 14:51:18 -04:00
parent 89315a3e9d
commit 585bec1a2a
Signed by: core
GPG Key ID: FDBF740DADDCEECF
12 changed files with 204 additions and 277 deletions

View File

@ -39,7 +39,7 @@
<hr>
<div class="nav-item">
<button class="nav-link py-2 px-4" on:click={() => {window.localStorage.setItem("mfa", "")}}>
<button class="nav-link py-2 px-4" on:click={() => {window.localStorage.setItem("mfa", ""); window.location.href = "/2fa"}}>
<i class="me-2 fas fa-right-from-bracket fa-fw"></i>
{$t("common.logout")}
</button>

17
tfweb/src/lib/Tooltips.js Normal file
View File

@ -0,0 +1,17 @@
import {browser} from "$app/environment";
export function updateTooltips() {
if (browser) {
setTimeout(() => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => {
let tooltip = new document.B.Tooltip(tooltipTriggerEl, {trigger: 'hover'})
tooltipTriggerEl.addEventListener('click', () => {
tooltip.hide();
tooltip.dispose();
})
});
console.log(tooltipList);
});
}
}

View File

@ -1,32 +0,0 @@
{
"itworks": {
},
"login": {
},
"signup": {
},
"ml": {
},
"2fasetup": {
},
"2fa": {
},
"networkcreate": {
},
"hosts": {
},
"roles": {
},
"common": {
}
}

View File

@ -7,6 +7,7 @@
"edit": "Edit",
"enrollbtn": "Enroll",
"deletehost": "Delete",
"blockhost": "Block",
"config": "Configuration",
"add": "Add",
"create": {
@ -37,5 +38,14 @@
"error": {
"ERR_DB_ERROR": "Removal failed"
}
},
"block": {
"title": "Are you sure you want to block the host {{host}}?",
"explainer": "The host will not be able to connect to the network once it is blocked. Re-enroll this host to unblock it.",
"confirm": "I'm sure",
"cancel": "Nevermind",
"error": {
"ERR_DB_ERROR": "Block failed"
}
}
}

View File

@ -15,6 +15,8 @@
"rules": "Rule count",
"description": "Description",
"actions": "Actions",
"editrole": "Edit",
"deleterole": "Delete",
"add": {
"title": "Create Role",
"any": "Any",

View File

@ -1,223 +0,0 @@
{
"itworks": {
"header": "Het werkt!",
"body": "Als u deze pagina ziet, is tfweb geïnstalleerd en (waarschijnlijk) correct geconfigureerd.",
"linkbody": "Misschien wilde je het {link0} of {link1}?",
"linkbody.link0": "beheerdersdashboard bezoeken",
"linkbody.link1": "een account aanmaken"
},
"login": {
"title": "Log in to your account",
"subtitle": "We'll send you an email with a \"magic link\".",
"label": "What is your email?",
"button": "Log in",
"email": "Check your email",
"emailbody": "We sent you an email with a link to complete logging in.",
"emailbody2": "Didn't work? Check your junk inbox or click {link0} to try again.",
"emailbody2.link0": "here",
"error": {
"invalidEmail": "That email address isn't valid. Try again.",
"generic": "There was an error logging you in. Try again or contact support with the error code {err}",
"usermissing": "That user does not exist."
},
"need": "Don't have an account? {link0}",
"need.link0": "Signup"
},
"signup": {
"title": "Create an account",
"subtitle": "We'll send you an email with a \"magic link\"",
"label": "What is your email?",
"button": "Create account",
"email": "Check your email",
"emailbody": "We sent you an email with a link to complete signing up.",
"emailbody2": "Didn't work? Check your junk inbox or click {link0} to try again.",
"emailbody2.link0": "here",
"error": {
"invalidEmail": "That email address isn't valid. Try again.",
"generic": "There was an error logging you in. Try again or contact support with the error code {err}",
"userexists": "That user already exists. Try {link0}?",
"userexists.link0": "logging in"
},
"already": "Already have an account? {link0}",
"already.link0": "Login"
},
"ml": {
"header": "Authenticated!",
"body": "Redirecting to admin page...",
"error": {
"notoken": "magic link token missing",
"badtoken": "token is invalid or has expired"
}
},
"2fasetup": {
"title": "Configure two-factor authentication",
"body": "Trifid requires all accounts to use TOTP two-factor authentication to help ensure the security of your network.",
"scan": "Scan this QR code with an authenticator app, like Authy or Google Authenticator.",
"code": "Or, enter the TOTP secret below into an authenticator app.",
"verify": "Verify by entering the 6-digit code shown in your authenticator into the box below.",
"button": "Enable 2FA",
"error": {
"generic": "Unable to enable 2fa: {err}",
"api": "Unable to contact the server. Try again later.",
"ERR_UNAUTHORIZED": "Incorrect 2FA code"
}
},
"2fa": {
"title": "Authenticate with TOTP",
"subtitle": "Enter the 6-digit code displayed in your authenticator app",
"label": "TOTP Code",
"button": "Verify",
"error": {
"ERR_UNAUTHORIZED": "Invalid 2FA code"
}
},
"networkcreate": {
"title": "Create your network",
"explain": "This defines what IP addresses will be assigned to your devices. The range you enter below must fall within either the RFC 1918 Private Address Space or the RFC 6598 Shared Address Space. Enter your network via CIDR notation.",
"label": "Network range",
"button": "Create network",
"valid": "Valid - {numIps} addresses ({start} to {end})",
"invalid": "Invalid",
"error": {
"generic": "Unable to create network: {err}",
"api": "Unable to contact the server. Try again later."
}
},
"hosts": {
"name": "Name",
"role": "Role",
"lastseen": "Last Seen",
"actions": "Actions",
"ipaddr": "IP Address",
"edit": "Edit",
"enrollbtn": "Enroll",
"deletehost": "Delete",
"config": "Configuration",
"add": "Add",
"create": {
"name": "Host name",
"btn": "Add host",
"cancel": "Cancel",
"role": "Role",
"ip": "IP Address",
"error": {
"needsname": "Host name is required.",
"needsrole": "Role is required.",
"invalidip": "Invalid IP address",
"ERR_DUPLICATE_VALUE": "IP address already in use"
}
},
"enroll": {
"title": "Enrolling host {host}",
"explainer": "Enrolling this host will allow it to communicate on your network. You'll need to have compatible client software installed first.",
"done": "Done, return me to the hosts page",
"cancel": "Nevermind",
"code": "Input {code} as your enrollment code into any trifid-compatible client software to enroll. It will expire in {expires} minutes."
},
"delete": {
"title": "Are you sure you want to delete the host {host}?",
"explainer": "This action cannot be undone. This host will still be able to connect to the network unless you block it first.",
"confirm": "I'm sure",
"cancel": "Nevermind",
"error": {
"ERR_DB_ERROR": "Removal failed"
}
}
},
"roles": {
"delete": {
"title": "Are you sure you want to delete the role {rule}?",
"explainer": "This action cannot be undone. This role must be removed from all hosts, lighthouses, and relays prior to deleting it.",
"confirm": "I'm sure",
"cancel": "Nevermind",
"error": {
"ERR_DB_ERROR": "Removal failed (role was probably still attached to a host)"
}
},
"create": "Add",
"explain": "Roles control how hosts, lighthouses, and relays communicate through firewall rules.",
"noroles": "You don't have any roles. You'll need to add at least one before you can add any hosts.",
"name": "Name",
"rules": "Rule count",
"description": "Description",
"actions": "Actions",
"add": {
"title": "Create Role",
"any": "Any",
"name": "Role name",
"desc": "Role description",
"button": "Create role",
"cancel": "Cancel",
"rules": "Inbound firewall rules",
"rulesexplainer": "Inbound traffic is denied by default. Add rules to allow traffic from hosts belonging to specific roles.",
"rulescols": {
"description": "Description",
"protocol": "Protocol",
"portrange": "Port range",
"allowedrole": "Allowed role",
"actions": "Actions"
},
"rulesadd": "Add rule",
"editrule": {
"protocol": "Protocol",
"range": "Port or port range",
"role": "Allowed role",
"desc": "Description",
"add": "Add rule",
"edit": "Save edit",
"cancel": "Cancel"
},
"error": {
"needsname": "Role Name is required"
}
},
"edit": {
"title": "Editing role {rule}",
"any": "Any",
"name": "Role name",
"desc": "Role description",
"button": "Save",
"cancel": "Cancel",
"rules": "Inbound firewall rules",
"rulesexplainer": "Inbound traffic is denied by default. Add rules to allow traffic from hosts belonging to specific roles.",
"rulescols": {
"description": "Description",
"protocol": "Protocol",
"portrange": "Port range",
"allowedrole": "Allowed role",
"actions": "Actions"
},
"ruleremove": "Remove rule",
"ruleedit": "Edit rule",
"rulesadd": "Add rule",
"editrule": {
"protocol": "Protocol",
"range": "Port or port range",
"role": "Allowed role",
"desc": "Description",
"add": "Add rule",
"edit": "Save edit",
"cancel": "Cancel"
}
}
},
"common": {
"title": "{title} | Trifid Web UI",
"page": {
"itworks": "It Works!",
"admin": "Admin Panel",
"login": "Login",
"ml": "Verify Magic Link",
"2fasetup": "Configure TOTP",
"2fa": "2-Factor Authentication",
"networkcreate": "Create Network",
"hosts": "Hosts",
"roles": "Roles",
"lighthouses": "Lighthouses",
"relays": "Relays",
"addhost": "Add Host"
},
"logout": "Log out",
"loading": "Dashboard is loading"
}
}

View File

@ -1,16 +1,9 @@
import i18n from 'sveltekit-i18n';
import type {Config} from 'sveltekit-i18n';
function buildLoader(locale: string, key: string, file: string) {
return {
locale: locale,
key: key,
loader: async () => ( await import (file)).default
}
}
import type {Config} from 'sveltekit-i18n'
const config: Config = ({
loaders: [
// ENGLISH //
{
locale: 'en',
key: '2fa',
@ -61,6 +54,7 @@ const config: Config = ({
key: 'signup',
loader: async () => (await import('./locales/en/signup.json')).default
},
// END ENGLISH //
]
})

View File

@ -6,12 +6,13 @@
import {onMount} from "svelte";
onMount(async () => {
const popper = await import("@popperjs/core");
const bootstrap = await import("bootstrap/dist/js/bootstrap.js");
document.onload(() => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
});
document.B = bootstrap;
document.P = popper;
})
</script>

View File

@ -1,9 +1,21 @@
import type { LayoutLoad } from './$types'
import {loadTranslations} from "$lib/i18n/translations";
import {browser} from "$app/environment";
export const load: LayoutLoad = async ({url}) => {
const { pathname } = url;
const initLocale = 'en';
let locale = 'en';
// determine locale
if (browser) {
if (window.localStorage.getItem("locale") != null) {
locale = window.localStorage.getItem("locale");
}
window.localStorage.setItem("locale", locale);
}
const initLocale = locale;
await loadTranslations(initLocale, pathname);
return {};
}

View File

@ -10,6 +10,7 @@
import type {Host} from "$lib/api/models/Host.ts";
import type {Role} from "$lib/api/models/Role.ts";
import AdminLayout from "$components/AdminLayout.svelte";
import {updateTooltips} from "$lib/Tooltips";
let loading = true;
let isError = false;
@ -83,6 +84,7 @@
roles = (await rolesApi.rolesList()).data!;
loading = false;
updateTooltips();
});
function getRoleName(byId: string): string {
@ -118,16 +120,22 @@
{#each hosts as host}
{#if !(host.isLighthouse || host.isRelay)}
<tr>
<td><a href="/hosts/{host.id}/edit">{host.name}</a></td>
<td>
<a href="/hosts/{host.id}/edit">{host.name}</a>
{#if host.isBlocked}
<u class="text-danger" data-bs-toggle="tooltip" data-bs-title="Re-enroll to unblock"><i class="fas fa-fw fa-ban"></i> Blocked</u>
{/if}
</td>
<td>{host.metadata?.lastSeenAt}</td>
<td>{host.ipAddress}</td>
<td><a href="/roles/{host.roleID}/edit">{getRoleName(host.roleID)}</a></td>
<td>
<div class="btn-group">
<a href="/hosts/{host.id}/enroll" title="{$t('hosts.enrollbtn')}" class="btn btn-success"><i class="fas fa-arrows-rotate fa-fw"></i></a>
<a href="/hosts/{host.id}/edit" title="{$t('hosts.edit')}" class="btn btn-primary"><i class="fas fa-pencil fa-fw"></i></a>
<a href="/hosts/{host.id}/edit/config" title="{$t('hosts.config')}" class="btn btn-info"><i class="fas fa-gear fa-fw"></i></a>
<a href="/hosts/{host.id}/delete" title="{$t('hosts.deletehost')}" class="btn btn-danger"><i class="fas fa-trash fa-fw"></i></a>
<a href="/hosts/{host.id}/enroll" title="{$t('hosts.enrollbtn')}" data-bs-toggle="tooltip" class="btn btn-success"><i class="fas fa-arrows-rotate fa-fw"></i></a>
<a href="/hosts/{host.id}/edit" title="{$t('hosts.edit')}" data-bs-toggle="tooltip" class="btn btn-primary"><i class="fas fa-pencil fa-fw"></i></a>
<a href="/hosts/{host.id}/edit/config" title="{$t('hosts.config')}" data-bs-toggle="tooltip" class="btn btn-info"><i class="fas fa-gear fa-fw"></i></a>
<a href="/hosts/{host.id}/delete" title="{$t('hosts.deletehost')}" data-bs-toggle="tooltip" class="btn btn-danger"><i class="fas fa-trash fa-fw"></i></a>
<a href="/hosts/{host.id}/block" title="{$t('hosts.blockhost')}" data-bs-toggle="tooltip" class="btn btn-secondary"><i class="fas fa-ban fa-fw"></i></a>
</div>
</td>
</tr>

View File

@ -0,0 +1,136 @@
<script lang="ts">
import {loading as tLoading, t} from "$lib/i18n/translations";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {APIResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
import type {APIError} from "$lib/auth.ts";
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi, HostsApi, FirewallRuleProtocolEnum, ResponseError} from "$lib/api";
import type {FirewallRule} from "$lib/api";
import AdminBar from "$components/AdminLayout.svelte";
import {page} from "$app/stores";
import AdminLayout from "$components/AdminLayout.svelte";
import {load} from "../../../+layout";
import type {HostGet200Response} from "$lib/api";
let loading = true;
let fullPageLoading = true;
let isError = false;
let error = '';
$: currentlyLoading = $tLoading || fullPageLoading;
logSetup();
let logger = new Logger("hosts/block/+page.svelte");
let hosts;
let host: HostGet200Response = {
data: {
name: 'Loading'
}
};
let formErr = '';
let hasFormErr = false;
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == APIResult.Failed) {
let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login';
return;
}
let mfa_load_info = await isAuthedMFA();
if (mfa_load_info[0] == APIResult.Failed) {
let err = mfa_load_info[1] as APIError;
logger.error(`mfa load failed: ${err.code} ${err.message}`);
window.location.href = '/2fa';
return;
}
// pull networks
const configuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const networksApi = new NetworksApi(configuration);
let networks;
try {
networks = await networksApi.networksList();
} catch (e) {
let resp_json = await e.response.json();
if (resp_json.errors[0].code == "ERR_NO_ORG") {
window.location.href = "/networkcreate";
return;
} else {
isError = true;
error = $t("networkcreate.error.generic", {err:resp_json.errors[0].code});
loading = false;
return;
}
}
console.log(networks);
if (networks.data?.length == 0) {
window.location.href = '/networkcreate';
return;
}
const hostsApi = new HostsApi(configuration);
hosts = await hostsApi.hostsList();
// pull our role
host = await hostsApi.hostGet({
hostID: $page.params.host_id
});
fullPageLoading = false;
loading = false;
});
async function blockRole() {
loading = true;
const configuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const hostsApi = new HostsApi(configuration);
try {
await hostsApi.hostBlock({hostID: $page.params.host_id});
} catch (e) {
let body = await (<ResponseError>e).response.json();
console.log(body);
formErr = $t("hosts.block.error." + body.errors[0].code);
hasFormErr = true;
loading = false;
return;
}
window.location.href = "/hosts";
}
</script>
<svelte:head>
<title>{$t("common.title", {title: $t("common.page.hosts")})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<AdminLayout selected="hosts">
<h3>{$t("hosts.block.title", {host: host.data.name})}</h3>
<p>{$t("hosts.block.explainer")}</p>
{#if loading}
<button disabled class="btn btn-danger"><i class="fas fa-gear fa-spin"></i></button>
{:else}
<button on:click={blockRole} class="btn btn-danger">{$t("hosts.block.confirm")}</button>
{/if}
<button on:click={() => {window.location.href = "/hosts"}} class="btn btn-outline-info">{$t("hosts.block.cancel")}</button>
{#if hasFormErr}
<p class="text-danger">{formErr}</p>
{/if}
</AdminLayout>
</LoadingWrapper>

View File

@ -8,6 +8,7 @@
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi, RolesApi} from "$lib/api";
import AdminLayout from "$components/AdminLayout.svelte";
import {updateTooltips} from "$lib/Tooltips";
let loading = true;
let isError = false;
@ -71,6 +72,7 @@
console.log(roles);
loading = false;
updateTooltips();
})
async function roleAdd() {
@ -115,8 +117,8 @@
<td>{role.description}</td>
<td>
<div class="btn-group">
<a href="/roles/{role.id}/edit" class="btn btn-primary"><i class="fas fa-pencil fa-fw"></i></a>
<a href="/roles/{role.id}/delete" class="btn btn-danger"><i class="fas fa-trash fa-fw"></i></a>
<a href="/roles/{role.id}/edit" title="{$t('roles.editrole')}" data-bs-toggle="tooltip" class="btn btn-primary"><i class="fas fa-pencil fa-fw"></i></a>
<a href="/roles/{role.id}/delete" title="{$t('roles.deleterole')}" data-bs-toggle="tooltip" class="btn btn-danger"><i class="fas fa-trash fa-fw"></i></a>
</div>
</td>