webui skeleton

This commit is contained in:
c0repwn3r 2023-05-30 18:50:47 -04:00
parent bce97ffe0b
commit 36c0bce365
Signed by: core
GPG Key ID: FDBF740DADDCEECF
54 changed files with 685 additions and 3297 deletions

View File

@ -1,20 +1,29 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended'
],
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
}
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

2
tfweb/.gitignore vendored
View File

@ -8,4 +8,4 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
src/lib/config.ts
.idea

View File

@ -1 +1,2 @@
engine-strict=true
resolution-mode=highest

38
tfweb/README.md Normal file
View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@ -8,28 +8,23 @@
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"typesafe-i18n": "typesafe-i18n"
"lint": "eslint ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.28.0",
"eslint-plugin-svelte3": "^4.0.0",
"postcss": "^8.4.21",
"eslint-plugin-svelte": "^2.26.0",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"tailwindcss": "^3.2.7",
"tslib": "^2.4.1",
"typescript": "^4.9.5",
"vite": "^4.0.0"
"typescript": "^5.0.0",
"vite": "^4.3.0"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.1",
"a17t": "^0.10.1"
"svelte-i18n": "^3.6.0"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,14 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
input[type=number].appearance-none::-webkit-inner-spin-button,
input[type=number].appearance-none::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number].appearance-none {
-moz-appearance:textfield;
}
}

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="dark:bg-slate-900 bg-white text-zinc-900 dark:text-zinc-50">
<div style="display: contents;">%sveltekit.body%</div>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -1,6 +0,0 @@
<script lang="ts">
export let tone = "~neutral";
export let priority = "";
</script>
<span class="badge {tone} {priority}"><slot/></span>

View File

@ -1,14 +0,0 @@
<script lang="ts">
export let title;
export let subtitle;
</script>
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{title}</h1>
<h2 class="ftext-sm">{subtitle}</h2>
</div>
</div>
</div>

View File

@ -1,217 +0,0 @@
<script lang="ts">
import {t} from "$lib/i18n";
import {getCurrentLocale, locales} from "$lib/i18n";
import {locale} from "$lib/stores/LocaleStore";
import {theme} from "$lib/stores/ThemeStore";
import {devmode} from "$lib/stores/DevmodeStore";
import {getCookie} from "$lib/cookie";
import {setCookie} from "$lib/cookie";
import {get_user_info, renderDevMenu} from "$lib/auth";
import {browser} from "$app/environment";
let loggedin;
if (browser) { loggedin = getCookie("authToken") !== "" } else { loggedin = false };
function toggleTheme() {
if ($theme === "dark") {
theme.set("light");
} else {
theme.set("dark");
}
}
function setLocale(newLocale: string) {
locale.set(newLocale);
window.location.reload();
}
let langDropdownOpen = false;
function toggleLangDropdown() {
langDropdownOpen = !langDropdownOpen;
}
function closeLangIfOpen() {
if (langDropdownOpen) {
langDropdownOpen = !langDropdownOpen;
}
}
let devDropdownOpen = false;
function toggleDevDropdown() {
devDropdownOpen = !devDropdownOpen;
}
function closeDevIfOpen() {
if (devDropdownOpen) {
devDropdownOpen = !devDropdownOpen;
}
}
let enableConfirm = false;
</script>
<header>
<nav class="flex items-center justify-between flex-wrap slate-300 dark:bg-slate-800 p-6 shadow-md">
<div>
<span class="font-semibold text-xl tracking-tight">{t("common.appName")}</span>
{#if loggedin}
<a class="text-md ml-5 decoration-none hover:underline" href="/admin">Organizations</a>
<a class="text-md ml-5 decoration-none hover:underline" href="/settings">Settings</a>
{/if}
</div>
<div class="inline-block text-sm leading-none lg:m-0">
<div class="relative inline-block">
<div>
<button on:click={toggleLangDropdown} type="button"
class="inline-flex w-full justify-center rounded-md px-4 py-2" id="lang-menu-button"
aria-expanded="true" aria-haspopup="true" title="{t('header.changeLang')}">
<i class="fa-solid fa-language"></i>
</button>
</div>
<div class:hidden={!langDropdownOpen}
class="transition absolute right-0 z-10 w-min mt-2 origin-top-right rounded-md bg-slate-100 dark:bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu" aria-orientation="vertical" aria-labelledby="lang-menu-button" tabindex="-1"
on:mouseleave={closeLangIfOpen}>
<div class="p-2" role="none">
<div class="whitespace-nowrap rounded p-2 bg-slate-200 dark:bg-slate-600" role="menuitem"
tabindex="-1">
<div>
<span class="mr-3 fi fi-{t('common.flag')}"></span>
<span class="mt-0.2 mr-0.5">{t("common.localeName")} <span
class="text-slate-400">- {t("common.selected")}</span></span>
</div>
</div>
{#each Object.keys(locales) as locale, i}
{#if getCurrentLocale() !== locale}
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {setLocale(locale)}}>
<div>
<span class="mr-3 fi fi-{t('common.flag', locale)}"></span>
<span class="mt-0.2">{t("common.localeName", locale)}</span>
</div>
</div>
{/if}
{/each}
</div>
</div>
</div>
{#if $theme === 'dark'}
<button title="{t('header.lightMode')}" class="inline-block text-sm px-4 leading-none mt-4 lg:mt-0"
on:click={toggleTheme}>
<i class="fa-solid fa-sun"></i>
</button>
{:else}
<button title="{t('header.darkMode')}" class="inline-block text-sm px-4 leading-none mt-4 lg:mt-0"
on:click={toggleTheme}>
<i class="fa-solid fa-moon"></i>
</button>
{/if}
{#if renderDevMenu()}
<button title="Developer menu" class="inline-block text-sm px-4 leading-none mt-4 lg:mt-0"
id="dev-menu-button" aria-expanded="true" aria-haspopup="true"
on:click={toggleDevDropdown}>
<i class="fa-solid fa-wrench"></i>
</button>
<div class:hidden={!devDropdownOpen}
class="transition absolute right-0 z-10 mr-10 w-max mt-2 origin-top-right rounded-md bg-slate-100 dark:bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu" aria-orientation="vertical" aria-labelledby="dev-menu-button" tabindex="-1"
on:mouseleave={closeDevIfOpen}>
{#if $devmode === "false"}
{#if !enableConfirm}
<div class="p-2" role="none">
<div class="rounded p-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {enableConfirm = true}}>
<div>
<span class="mt-0.2">Enable developer mode</span>
</div>
</div>
</div>
{:else}
<div class="p-2" role="none">
<div class="rounded p-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {devmode.set(true)}}>
<div>
<span class="mt-0.2">Are you sure?</span>
</div>
</div>
</div>
{/if}
{:else}
<div class="p-2" role="none">
<div class="rounded p-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {navigator.clipboard.writeText(getCookie("sessionToken"))}}>
<div>
<span class="mt-0.2">Copy session token</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {navigator.clipboard.writeText(getCookie("authToken"))}}>
<div>
<span class="mt-0.2">Copy mfa token</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {navigator.clipboard.writeText(getCookie("sessionToken") + " " + getCookie("authToken"))}}>
<div>
<span class="mt-0.2">Copy API key</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {setCookie("authToken", "", -1); window.location.reload()}}>
<div>
<span class="mt-0.2">Clear MFA state</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {setCookie("authToken", "", -1); setCookie("sessionToken", "", -1); window.location.reload()}}>
<div>
<span class="mt-0.2">Clear login state</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={async () => {await navigator.clipboard.writeText(JSON.stringify(await get_user_info(getCookie("sessionToken") + " " + getCookie("authToken"))))}}>
<div>
<span class="mt-0.2">Copy user profile</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {devmode.set("false")}}>
<div>
<span class="mt-0.2">Disable developer mode</span>
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
</nav>
</header>

View File

@ -1,29 +0,0 @@
<script>
import { onMount } from 'svelte';
export let codeValue;
let qrcode;
onMount(() => {
let script = document.createElement('script');
script.src = "https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"
document.head.append(script);
script.onload = function() {
qrcode = new QRCode("qrcode", {
text: codeValue,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
};
});
</script>
<div id="qrcode" class="w-max"></div>

View File

@ -1,15 +0,0 @@
<script lang="ts">
</script>
<svelte:head>
<link rel="stylesheet" href="https://cdn.e3t.cc/fa/6.2.0/css/all.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@6.6.6/css/flag-icons.min.css"/>
<script>
if (localStorage.theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
</script>
</svelte:head>

View File

@ -1,44 +0,0 @@
<script lang="ts">
export let selected;
export let orgid;
let hostsstyle = (selected == "hosts" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
let lighthousestyle = (selected == "lighthouses" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
let relaysstyle = (selected == "relays" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
let rolesstyle = (selected == "roles" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
</script>
<div class="min-h-screen flex flex-row overflow-y-hidden">
<div class="max-h flex flex-col w-56 overflow-hidden border-r-2 border-slate-500">
<ul class="flex flex-col py-4">
<li>
<a href="/org/{orgid}/hosts" class="{hostsstyle} mt-2 flex flex-row items-center h-12 transition">
<span class="ml-10 text-sm font-medium">Hosts</span>
</a>
</li>
<li>
<a href="/org/{orgid}/lighthouses" class="{lighthousestyle} mt-2 flex flex-row items-center h-12 dark:hover:bg-slate-600 hover:bg-slate-200 transition">
<span class="ml-10 text-sm font-medium">Lighthouses</span>
</a>
</li>
<li>
<a href="/org/{orgid}/relays" class="{relaysstyle} mt-2 flex flex-row items-center h-12 dark:hover:bg-slate-600 hover:bg-slate-200 transition">
<span class="ml-10 text-sm font-medium">Relays</span>
</a>
</li>
<li>
<a href="/org/{orgid}/roles" class="{rolesstyle} mt-2 flex flex-row items-center h-12 dark:hover:bg-slate-600 hover:bg-slate-200 transition">
<span class="ml-10 text-sm font-medium">Roles</span>
</a>
</li>
</ul>
</div>
<div class="overflow-hidden w-full min-w-screen min-h-screen ml-5 mr-5 m-2">
<slot></slot>
</div>
</div>

10
tfweb/src/hooks.server.ts Normal file
View File

@ -0,0 +1,10 @@
import type { Handle } from '@sveltejs/kit'
import { locale } from 'svelte-i18n'
export const handle: Handle = async ({ event, resolve }) => {
const lang = event.request.headers.get('accept-language')?.split(',')[0]
if (lang) {
locale.set(lang)
}
return resolve(event)
}

View File

@ -8,4 +8,4 @@ export function persist(name: string, def_val = ""): Writable<any> {
if (browser) return (localStorage.setItem(name, value));
});
return store;
}
}

View File

@ -1,146 +0,0 @@
import {fetch_timeout} from "./util";
import {API_ROOT} from "./config";
import {Logger, logSetup} from "./logger";
import {getCookie, setCookie} from "./cookie";
import {browser} from "$app/environment";
logSetup();
const logger = new Logger("auth.ts");
export function redact_token(token: string) {
const stars = "*".repeat(token.length - 5);
return token.substring(0, 5) + stars;
}
export interface UserInfo {
data: UserData,
metadata: object
}
export interface UserData {
actorType: string,
actor: Actor
}
export interface Actor {
id: string,
organizationID: string,
email: string,
createdAt: string,
hasTOTPAuthenticator: string
}
export async function get_user_info(api_key: string): Promise<UserInfo | string> {
logger.info("Asking server for user information");
try {
const resp = await fetch_timeout(`${API_ROOT}/v2/whoami`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${api_key}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`error fetching user information: ${rawerror}`);
return rawerror;
}
return JSON.parse(await resp.text()) as UserInfo;
} catch (e) {
logger.error(`Error fetching userinfo: ${e}`);
return `${e}`
}
}
export async function enforce_session(): Promise<[boolean, string]> {
logger.info("Checking session authentication");
const session_token = getCookie("sessionToken");
if (session_token === "") {
logger.error("No session token is present");
setCookie("sessionToken", "", -1);
return [false, ""];
}
logger.info(`Session token is ${redact_token(session_token)}`);
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/auth/check_session`, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session_token}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`session token is invalid: ${rawerror}`);
setCookie("sessionToken", "", -1);
return [false, rawerror];
} else {
logger.info("session token OK");
// session ok
return [true, session_token];
}
} catch (e) {
// error in http request
logger.error(`session token is invalid: ${e}`);
setCookie("sessionToken", "", -1);
return [false, `${e}`]
}
}
export async function enforce_auth(): Promise<[boolean, string]> {
logger.info("Checking mfa authentication");
const session_result = await enforce_session();
if (!session_result[0]) {
// session token is invalid
logger.error("Session token is invalid, therefore auth token cannot be valid");
return [false, session_result[1]];
}
const session_token = session_result[1];
const auth_token = getCookie("authToken");
if (auth_token === "") {
logger.error("No auth token is present");
setCookie("authToken", "", -1);
return [false, ""];
}
logger.info(`MFA token is ${redact_token(auth_token)}`);
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/auth/check_auth`, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session_token} ${auth_token}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
setCookie("authToken", "", -1);
return [false, rawerror];
} else {
// session ok
logger.info("MFA token is OK");
if (browser) {
document.documentElement.classList.add("loggedin");
}
return [true, `${session_token} ${auth_token}`];
}
} catch (e) {
// error in http request
setCookie("authToken", "", -1);
return [false, `${e}`]
}
}
export function renderDevMenu() {
if (!browser) return false;
if (localStorage.getItem("allowdev") === "HACKERMAN") {
console.log("allowing dev menu");
return true;
}
console.log("not allowing dev menu");
return false;
}

View File

@ -1,2 +0,0 @@
// Set this to the API root of your trifid-api instance.
export const API_ROOT = "http://localhost:8000";

View File

@ -1,2 +0,0 @@
// Set this to the API root of your trifid-api instance.
export const API_ROOT = "http://localhost:8000";

View File

@ -1,22 +0,0 @@
export function setCookie(name: string, value: string, expires: number) {
const d = new Date();
d.setTime(d.getTime() + expires);
const expires_at = "expires="+ d.toUTCString();
document.cookie = name + "=" + value + ";" + expires_at + ";path=/";
}
export function getCookie(name: string): string {
const name_with_equals = name + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name_with_equals) == 0) {
return c.substring(name_with_equals.length, c.length);
}
}
return "";
}

View File

@ -1,50 +0,0 @@
import {locale} from "$lib/stores/LocaleStore";
import {get} from "svelte/store";
import en from "$lib/i18n/en.json";
import jp from "$lib/i18n/jp.json";
import nl from "$lib/i18n/nl.json";
import {Logger, logSetup} from "$lib/logger";
logSetup();
const logger = new Logger("i18n");
export const locales: any = {
"en": en,
"jp": jp,
"nl": nl
}
export function getCurrentLocale(): string {
return get(locale);
}
export function setLocale(newLocale: string) {
logger.info(`Setting locale to ${newLocale}`);
if (!(newLocale in locales)) {
logger.error(`Locale ${newLocale} does not exist`);
return false;
}
locale.set(newLocale);
return true;
}
export function t(query: string, locale = ""): string {
let val: any = locales[locale === "" ? getCurrentLocale() : locale];
const spl: Array<string> = query.split('.');
for (let i = 0; i < spl.length; i++) {
const item = spl[i];
if (item in val) {
val = val[item];
} else {
logger.info(`missing locale string ${query}`);
val = "missing: " + query;
break;
}
}
if (typeof val === "object") {
logger.info(`query string ${query} is not a message`);
return "missing: " + query;
}
return val;
}

View File

@ -1,153 +0,0 @@
{
"common": {
"localeName": "English",
"appName": "trifid",
"flag": "gb",
"selected": "selected"
},
"header": {
"lightMode": "Light mode",
"darkMode": "Dark mode",
"changeLang": "Change language"
},
"login": {
"title": "Log in to your account",
"subtitle": "We'll send you an email with a \"magic link\".",
"prompt": "What's your email?",
"actionButtonText": "Log in",
"createaccount": "create an account",
"sentMagicLink": "Check your email!",
"magicLinkExplainer": "We sent you a link, click on it to continue logging in.",
"apierror": {
"authorization was provided but it is expired or invalid": "User does not exist, maybe consider creating an account?",
"TypeError": "unable to contact server, please try again later"
}
},
"signup": {
"title": "Get started with trifid",
"subtitle": "Simply manage connectivity between hosts running any major OS.",
"prompt": "What is your email address?",
"actionButtonText": "Create account",
"login": "Already have an account?",
"sentMagicLink": "Check your email!",
"magicLinkExplainer": "We sent you a link, click on it to finish creating your account."
},
"magiclink": {
"loadtitle": "Verifying magic link...",
"loadsubtitle": "Hang on while we check this link.",
"failed": "Unable to verify magic link",
"finished": "Magic link verified!",
"redirecting": "Redirecting...",
"missing_email": "Your request didn't contain your email. Make sure you have the entire link.",
"missing_token": "Your request didn't contain your auth token. Make sure you have the entire link.",
"tryAgain": "Try logging in again?",
"apierror": {
"unable to parse the request body, is it properly formatted?": "There was an error processing your request, please try again later.",
"this token is invalid - no rows returned by a query that expected to return at least one row": "This token is invalid or has expired."
}
},
"mfa": {
"title": "Two-factor authentication",
"subtitle": "Enter the six-digit code displayed on your authenticator app",
"actionButtonText": "Check code",
"apierror": {
"invalid TOTP code (maybe it expired?)": "Incorrect 2FA code"
}
},
"mfasetup": {
"title": "Protect your account",
"subtitle": "2FA is required for all trifid accounts. Protect your account with any TOTP-compatible authenticator app.",
"qrtitle": "Scan the QR code with your authenticator app.",
"secrettitle": "Or, copy this code into your authenticator app.",
"verifytitle": "Enter the six-digit code shown on your authenticator app",
"loadingmfa": "Hang on while we load your account...",
"actionButtonText": "Add authenticator",
"apierror": {
"Invalid TOTP code": "Incorrect 2FA code"
}
},
"error": {
"code": {
"404": "Not found :("
},
"message": {
"Not Found": "We couldn't find this page. Try going back to the homepage."
}
},
"neworg": {
"title": "Create an organization",
"subtitle": "An organization is how you manage devices on your network. You can only own one, but can join organizations created by others.",
"iprangeprompt": "What IP range(s) would you like to use?",
"actionButtonText": "Create organization",
"cidrhelp": "Please provide it in CIDR notation. If providing multiple, please comma-separate them.",
"firstcert": "Configure your first CA",
"cahelp": "These settings cannot be changed. Please be careful.",
"additionalConstraints": "Add additional constraints (advanced)",
"subnetprompt": "What subnets would you like to allow?",
"subnethelp": "Comma-separated list of subnets in CIDR notation. This will constrain which subnets can be applied to client certs. Default: empty (any)",
"groupprompt": "What groups would you like to allow?",
"grouphelp": "Comma-separated list of groups. This will constrain which groups can be applied to client certs. Default: empty (any)",
"apierror": {
"orgcreate": "Unable to create organization",
"cacreate": "Unable to create signing CA",
"TypeError: NetworkError when attempting to fetch resource": "Unable to contact the backend. Please try again later.",
"invaliddata": "The server was unable to parse your request. Reload the page and try again. Ensure all fields are correct."
}
},
"admin": {
"apierror": {
"loadownedorg": "Unable to load organizations",
"this user does not have permission to access this org": "You do not have permission to access one of the orgs associated with your account. Your access may have been revoked. Please reload the page."
}
},
"org": {
"apierror": {
"notyourorg": "Access denied",
"notyourorgexplainer": "You do not have permission to manage this org. Ask the owner of this org to add you, or go back to the homepage.",
"loadroles": "Unable to load roles",
"TypeError: NetworkError when attempting to fetch resource": "Unable to contact the server. Please try again later",
"Unable to create default role firewall rules": "Unable to create the default role. Please try again later."
},
"nohosts": "You don't have any hosts set up",
"nohostsdesc": "Create and enroll a host to begin using it in your network.",
"nohostsdesc2": "Be sure to also create and enroll a lighthouse, or your hosts wont be able to communicate.",
"nohosts_cta": "Add host",
"nolighthouses": "You don't have any lighthouses set up",
"nolighthousesdesc": "Lighthouses keep track of potential routes to overlay network hosts, and they assist in establishing connections between hosts.",
"nolighthouses_list_item1": "Each network needs at least one lighthouse.",
"nolighthouses_list_item2": "If you plan to access hosts over the Internet, at least one lighthouse will need a static, public IPv4 address with its firewall configured to allow inbound udp traffic on a specific port.",
"nolighthouses_list_item3": "A modestly-sized cloud instance should be sufficient for most users.",
"nolighthouses_list_item4": "Lighthouses are also hosts - they will have an IP and you will be able to access them over the network.",
"nolighthouses_cta": "Add lighthouse",
"norelays": "Adding a relay is recommended",
"norelays_list_item1": "Relays ensure connectivity for scenarios when direct connections fail.",
"norelays_list_item2": "Without a relay, some devices may be unable to communicate.",
"norelays_list_item3": "Your lighthouses also act as relays.",
"norelays_lighthouse": "You currently have {} lighthouses acting as relays",
"norelays_cta": "Add relay",
"roles_title": "Roles",
"roles_bar": "Roles control how hosts, lighthouses, and relays communicate through firewall rules.",
"roles_cta": "Add",
"guide": "Read the guide →",
"noroles": "Wow, such empty",
"noroles_cta": "You don't have any roles. Consider creating one with the button above.",
"roleaddprompt": "What's the name of your new role?",
"roleaddpromptdesc": "What's the description of your new role?",
"roleadd_cta": "Create role"
}
}

View File

@ -0,0 +1,12 @@
import { browser } from '$app/environment'
import { init, register } from 'svelte-i18n'
const defaultLocale = 'en'
register('en', () => import('./locales/en.json'))
//register('de', () => import('./locales/de.json'))
init({
fallbackLocale: defaultLocale,
initialLocale: browser ? window.navigator.language : defaultLocale,
})

View File

@ -1,22 +0,0 @@
{
"common": {
"localeName": "日本語",
"appName": "trifid",
"flag": "jp",
"selected": "選択済み"
},
"header": {
"lightMode": "ライトモード",
"darkMode": "ダークモード",
"changeLang": "言語の変更"
},
"login": {
"title": "あなたのアカウントにログイン",
"subtitle": "「マジックリンク」を記載したメールをお送りします。",
"prompt": "あなたのメールは何ですか?",
"actionButtonText": "ログイン",
"apierror": {
"authorization was provided but it is expired or invalid": "ユーザーが存在しません。アカウントの作成を検討してください。"
}
}
}

View File

@ -0,0 +1,8 @@
{
"home": {
"itworks": "It works!",
"itworkssub": "If you're seeing this page, tfweb is installed and (probably) correctly configured.",
"adminlink": "Perhaps you meant to visit the {link0}?",
"adminlink.link0": "admin panel"
}
}

View File

@ -1,22 +0,0 @@
{
"common": {
"localeName": "Nederlands",
"appName": "trifid",
"flag": "nl",
"selected": "Geselecteerd"
},
"header": {
"lightMode": "Lichtmodus",
"darkMode": "Donkere modus",
"changeLang": "Taal wijzigen"
},
"login": {
"title": "Log in op jouw account",
"subtitle": "We sturen je een e-mail met een \"magische link\".",
"prompt": "Wat is jouw e-mail?",
"actionButtonText": "Log in",
"apierror": {
"authorization was provided but it is expired or invalid": "Gebruiker bestaat niet, misschien overwegen om een account aan te maken?"
}
}
}

View File

@ -115,4 +115,4 @@ export class Logger {
debug(message: any) {
log(LEVEL_DEBUG, this.name, message)
}
}
}

View File

@ -1,77 +0,0 @@
import {Logger, logSetup} from "./logger";
import {fetch_timeout} from "./util";
import {API_ROOT} from "./config";
import type {UserInfo} from "./auth";
const logger = new Logger("orgs.ts");
logSetup();
export interface Organization {
org_id: number,
owner_id: number,
ca_crts: string,
authorized_users: number[],
name: string
}
export async function list_org(api_key: string): Promise<number[] | string> {
logger.info(`Getting list of orgs associated with this user`);
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/orgs`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${api_key}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`error fetching user information: ${rawerror}`);
return rawerror;
}
return JSON.parse(await resp.text()).org_ids;
} catch (e) {
logger.error(`Error fetching org list: ${e}`);
return `${e}`
}
}
export async function org(org: number, api_key: string): Promise<Organization | string> {
logger.info(`Getting information about org ${org}`);
let org_resp: Organization;
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/org/${org}`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${api_key}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`error fetching user information: ${rawerror}`);
return rawerror;
}
org_resp = JSON.parse(await resp.text()) as Organization;
} catch (e) {
logger.error(`Error fetching org list: ${e}`);
return `${e}`
}
logger.info(`Getting user email for ${org_resp.owner_id}`);
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/user/${org_resp.owner_id}`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${api_key}`
}
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`error fetching user information: ${rawerror}`);
return rawerror;
}
org_resp.name = JSON.parse(await resp.text()).email + "'s organization";
} catch (e) {
logger.error(`Error fetching owner email: ${e}`);
return `${e}`
}
return org_resp;
}

View File

@ -1,3 +0,0 @@
import { persist } from "$lib/PersistentStore";
export const locale = persist("lang", "en");

View File

@ -1,15 +0,0 @@
import {writable} from "svelte/store";
import {browser} from "$app/environment";
export const theme = writable(browser && localStorage.getItem("theme") || "light");
theme.subscribe((value: string) => {
if (browser) {
if (value === "dark") {
localStorage.setItem("theme", "dark");
document.documentElement.classList.add("dark");
} else {
localStorage.setItem("theme", "light");
document.documentElement.classList.remove("dark");
}
}
});

View File

@ -1,3 +1,3 @@
import { persist } from "$lib/PersistentStore";
export const devmode = persist("dev", "false");
export const devmode = persist("dev", "false");

View File

@ -1,92 +0,0 @@
import {fetch_timeout} from "./util";
import {Logger, logSetup} from "./logger";
import {API_ROOT} from "./config";
const logger = new Logger("totp.ts");
logSetup();
export interface TOTPSetupDetails {
totpToken: string,
secret: string,
url: string
}
export async function startTotpSetup(api_key: string): Promise<TOTPSetupDetails | string> {
logger.info("Starting TOTP setup");
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/totp-authenticators`, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
'body': "{}"
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`API returned error setting up TOTP: ${rawerror}`);
return rawerror;
}
logger.info('Initiated TOTP setup successfully');
return (await resp.json()).data as TOTPSetupDetails;
} catch (e) {
logger.error(`Error while trying to setup TOTP: ${e}`);
return `${e}`
}
}
export interface TOTPToken {
token: string
}
export async function finishTOTPSetup(api_key: string, token: string, code: string): Promise<TOTPToken | string> {
logger.info("Finishing up TOTP setup");
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/verify-totp-authenticator`, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
'body': `{"totpToken":"${token}","code":"${code}"}`
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`API returned error finishing up TOTP: ${rawerror}`);
return rawerror;
}
logger.info('Finished TOTP setup! Auth token issued');
return {
token: (await resp.json()).data.authToken
};
} catch (e) {
logger.error(`Error while trying to finish TOTP: ${e}`);
return `${e}`
}
}
export async function validateTOTP(api_key: string, code: string): Promise<TOTPToken | string> {
logger.info("Validating 2fa code");
try {
const resp = await fetch_timeout(`${API_ROOT}/v1/auth/totp`, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
'body': `{"code":"${code}"}`
});
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0].message;
logger.error(`API returned error verifying TOTP: ${rawerror}`);
return rawerror;
}
logger.info('auth token issued');
return {
token: (await resp.json()).data.authToken
};
} catch (e) {
logger.error(`Error while trying to validate TOTP: ${e}`);
return `${e}`
}
}

View File

@ -1,14 +0,0 @@
export async function fetch_timeout(resource: RequestInfo | URL, options: RequestInit | undefined = {}) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { timeout = 8000 } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
}

View File

@ -1,14 +0,0 @@
<script lang="ts">
import FullPageError from "../components/FullPageError.svelte";
import {t} from "$lib/i18n";
import {page} from "$app/stores";
import {onMount} from "svelte";
import {enforce_auth} from "$lib/auth";
onMount(async () => {
await enforce_auth(); // This is purely just to trigger loggedin for the navbar. We don't actually do anything with this.
})
</script>
<FullPageError title="{t('error.code.' + $page.status)}" subtitle="{t('error.message.' + $page.error.message)}"/>

View File

@ -1,11 +1 @@
<script lang="ts">
import "../app.css";
import Theme from '../components/Theme.svelte';
import Header from "../components/Header.svelte";
</script>
<Theme/>
<Header/>
<slot />

View File

@ -1 +1,11 @@
export const prerender = true;
import { browser } from '$app/environment'
import '$lib/i18n'
import { locale, waitLocale } from 'svelte-i18n'
import type { LayoutLoad } from './$types'
export const load: LayoutLoad = async () => {
if (browser) {
locale.set(window.navigator.language)
}
await waitLocale()
}

View File

@ -0,0 +1,7 @@
<script lang="ts">
import {t} from 'svelte-i18n';
</script>
<h1>{$t('home.itworks')}</h1>
<p>{$t('home.itworkssub')}</p>
<p>{@html $t('home.adminlink', {values:{link0:'<a href="/admin">'+$t('home.adminlink.link0')+'</a>'}})}</p>

View File

@ -1,167 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
let logger = new Logger("admin/+page.svelte");
logSetup();
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let org_list;
let user_info;
let owned_org_id;
let owned_org_orginfo;
let owns_org = false;
let orgs = {};
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET ORG LIST
// we need to know what orgs this user has access to
org_list = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loadorg');
fullPageErrorSubtitle = t('admin.apierror.' + org_list);
return;
}
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
owned_org_id = user_info.data.actor.organizationID;
if (owned_org_id !== "") {
// the user actually owns an org
logger.info("User owns an org");
owns_org = true;
owned_org_orginfo = await org(owned_org_id as number, api_token);
if (typeof owned_org_orginfo === "string") {
logger.error(owned_org_orginfo);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loadownedorg');
fullPageErrorSubtitle = t('admin.apierror.' + owned_org_orginfo);
return;
}
} else {
logger.info("User does not own an org");
}
// GET INFO ABOUT ALL THE ORGS
for (let i = 0; i < org_list.length; i++) {
let org_id = org_list[i];
if (org_id === owned_org_id) continue;
let org_info = await org(org_id as number, api_token);
if (typeof org_info === "string") {
logger.error(org_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loadunownedorg');
fullPageErrorSubtitle = t('admin.apierror.' + org_info);
return;
}
orgs[org_info.name] = org_info;
}
logger.info("Loaded " + org_list.length + " associated orgs, finish displayDataLoad");
})
function setOrg(to) {
window.location.href = "/org/" + to;
}
function newOrg() {
window.location.href = "/org/new";
}
function joinOrg() {
window.location.href = "/org/join";
}
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8 h-max">
<div class="w-full max-w-md m-auto">
<!-- org selector -->
<h1 class="font-semibold text-xl">Select an organization to manage</h1>
<div class="bg-slate-200 dark:bg-slate-700 rounded p-2 mt-2">
<div class="grid grid-cols-1 divide-width divide-y divide-black dark:divide-gray-100">
<div>
{#if owns_org}
<div class="cursor-pointer flow-root transition hover:bg-slate-300 dark:hover:bg-slate-600 rounded m-2 p-2 group"
on:click={() => {setOrg(owned_org_id)}}>
<span class="float-left cursor-pointer text-md">Your organization</span>
<span class="float-right cursor-pointer text-right text-md font-light text-slate-200 dark:text-slate-700 group-hover:text-slate-200">Next &rarr;</span>
</div>
{:else}
<div class="cursor-pointer flow-root transition hover:bg-slate-300 dark:hover:bg-slate-600 rounded m-2 p-2 group"
on:click={() => {newOrg()}}>
<span class="float-left cursor-pointer text-md">+ Create an organization</span>
</div>
{/if}
</div>
<!--
<div>
<h4 class="ml-0.5 text-xs font-bold text-gray-200 dark:text-gray-400 mt-0.5">Organizations you have access to</h4>
<div class="cursor-pointer flow-root transition hover:bg-slate-300 dark:hover:bg-slate-600 rounded m-2 p-2 group" on:click={() => {setOrg("1")}}>
<span class="float-left cursor-pointer text-md">core@e3t.cc's organization</span>
<span class="float-right cursor-pointer text-right text-md font-light text-slate-200 dark:text-slate-700 group-hover:text-slate-200">Next &rarr;</span>
</div>
<div class="cursor-pointer flow-root transition hover:bg-slate-300 dark:hover:bg-slate-600 rounded m-2 p-2 group" on:click={() => {setOrg("1")}}>
<span class="float-left cursor-pointer text-md">core@coredoes.dev's organization</span>
<span class="float-right cursor-pointer text-right text-md font-light text-slate-200 dark:text-slate-700 group-hover:text-slate-200">Next &rarr;</span>
</div>
</div>
-->
<div>
<div class="cursor-pointer flow-root transition hover:bg-slate-300 dark:hover:bg-slate-600 rounded m-2 p-2 group"
on:click={() => {joinOrg()}}>
<span class="float-left cursor-pointer text-md">+ Join an existing organization</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,111 +0,0 @@
<script lang="ts">
import {t} from "$lib/i18n";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {Logger, logSetup} from "../../../lib/logger";
import {onMount} from "svelte";
import {enforce_session} from "$lib/auth";
let email = "";
let isloading = false;
let isFinished = false;
let hasError = false;
let error = "";
logSetup();
let logger = new Logger("login/+page.svelte");
async function generateMagicLink() {
if (isloading) {
return;
}
isloading = true;
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/auth/magic-link`, {
'method': 'POST',
'headers': {
'Content-Type': 'application/json'
},
'body': JSON.stringify({
email: email
})
});
if (!resp.ok) {
hasError = true;
const rawerror = JSON.parse(await resp.text()).errors[0].message;
error = t(`login.apierror.${rawerror}`);
isloading = false;
} else {
isloading = false;
isFinished = true;
}
} catch (e) {
hasError = true;
logger.error(`Error requesting magic link from api: ${e}`);
error = t(`login.apierror.${e.name}`);
isloading = false;
}
}
onMount(async () => {
let st_result = await enforce_session();
if (st_result[0]) {
// User already authed, redirect them
logger.info("User already logged in");
window.location.href = "/admin";
return;
}
})
</script>
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
{#if !isFinished}
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{t('login.title')}</h1>
<h2 class="ftext-sm">{t('login.subtitle')}</h2>
</div>
<!-- The actual form -->
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={generateMagicLink}>
<div class="rounded-md shadow-sm">
<label for="email">{t('login.prompt')}</label>
<input bind:value={email} id="email"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
{#if hasError}
<span class="text-red-600 text-sm">{error}</span>
{/if}
</div>
<button class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
{#if !isloading}
{t('login.actionButtonText')}
{:else}
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{/if}
</button>
<div class="text-xs mt-0.5">
<a class="font-bold text-purple-400 dark:text-purple-600" href="/signup">{t('login.createaccount')}</a>
</div>
</form>
{:else}
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{t('login.sentMagicLink')}</h1>
<h2 class="ftext-sm">{t('login.magicLinkExplainer')}</h2>
</div>
{/if}
</div>
</div>

View File

@ -1,99 +0,0 @@
<script lang="ts">
import {t} from "$lib/i18n";
import {API_ROOT} from "$lib/config";
import {onMount} from "svelte";
import {setCookie} from "$lib/cookie";
let isLoading = true;
let hasError = false;
let error = "";
onMount(() => {
// content loaded - start checking the api
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has("email")) {
error = t('magiclink.missing_email');
hasError = true;
isLoading = false;
return;
} else if (!urlParams.has("token")) {
error = t('magiclink.missing_token');
hasError = true;
isLoading = false;
return;
}
let token = urlParams.get("token");
let xhr = new XMLHttpRequest();
xhr.timeout = 10000;
xhr.open('POST', `${API_ROOT}/v1/auth/verify-magic-link`);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({
magicLinkToken: token
}));
xhr.ontimeout = () => {
hasError = true;
error = t('magiclink.apierror.timeout');
isLoading = false;
};
xhr.onload = () => {
if (xhr.status != 200) {
// error
hasError = true;
const rawerror = JSON.parse(xhr.responseText).errors[0].message;
error = t(`magiclink.apierror.${rawerror}`);
isLoading = false;
} else {
let resp = JSON.parse(xhr.responseText);
if (resp.data === undefined || resp.data.sessionToken === undefined) {
error = t("magiclink.apierror.badresponse");
hasError = true;
isLoading = false;
return;
}
let sess = resp.data.sessionToken;
// Set a really, really long expiry date. We will remove it when the server starts giving us unauthorized errors, but we don't know ahead of time what the server's expiration time is.
setCookie("sessionToken", sess, (86400 * 365));
isLoading = false;
hasError = false;
// redirect them to the homepage
window.location.href = "/admin"
}
};
xhr.onerror = () => {
hasError = true;
error = t('magiclink.apierror.xhrerror');
isLoading = false;
};
});
</script>
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
{#if isLoading}
<div>
<h1 class="font-semibold text-2xl">{t('magiclink.loadtitle')}</h1>
<h2 class="text-sm">{t('magiclink.loadsubtitle')}</h2>
</div>
{:else}
{#if hasError}
<h1 class="font-semibold text-2xl">{t('magiclink.failed')}</h1>
<h2 class="text-sm">{error}</h2>
<a class="text-xs font-bold text-purple-400 dark:text-purple-600" href="/auth/login">{t('magiclink.tryAgain')}</a>
{:else}
<h1 class="font-semibold text-2xl">{t('magiclink.finished')}</h1>
<h2 class="text-sm">{t('magiclink.redirecting')}</h2>
{/if}
{/if}
</div>
</div>

View File

@ -1,125 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {get_user_info} from "$lib/auth";
import type {UserInfo} from "$lib/auth";
import {t} from "$lib/i18n";
import {validateTOTP} from "$lib/totp";
import {setCookie} from "$lib/cookie";
import {enforce_auth} from "$lib/auth";
let logger = new Logger("auth/mfa/+page.svelte");
logSetup();
let isFinished = false;
let isloading = false;
let api_token = "";
let mfa_token = "";
let hasError = false;
let error = "";
onMount(async () => {
let st_res = await enforce_session();
if (!st_res[0]) {
// session token is invalid
// Session token is invalid. redirect to login
logger.info("Invalid session token, redirecting to login");
window.location = "/auth/login";
return;
}
api_token = st_res[1];
let at_res = await enforce_auth();
if (at_res[0]) {
logger.info("User already TOTP authenticated");
window.location = "/admin";
return;
}
let user: UserInfo | string = await get_user_info(api_token);
if (typeof user === "string") {
logger.error(`Unable to get user info: ${user}`);
hasError = true;
error = user;
return;
}
if (!user.data.actor.hasTOTPAuthenticator) {
logger.error('User doesn\'t have MFA setup yet, redirecting');
window.location = "/auth/mfasetup";
return;
}
});
async function tryMFACode() {
isloading = true;
logger.info(`Submitting 2FA verify with code ${mfa_token}`);
let resp = await validateTOTP(api_token, mfa_token);
if (typeof resp === "string") {
logger.error(`Unable to validate TOTP token: ${resp}`);
hasError = true;
isloading = false;
error = t(`mfa.apierror.${resp}`);
return;
}
// set cookie
setCookie("authToken", resp.token, 86400 * 365);
window.location = "/admin";
}
function validateKeypress(e: KeyboardEvent) {
if (e.charCode === 13) { return; }
if (e.charCode < 47 || e.charCode > 57) {
e.preventDefault();
}
if (e.target.value.length >= 6) {
e.preventDefault();
}
}
</script>
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
{#if !isFinished}
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{t('mfa.title')}</h1>
<h2 class="ftext-sm">{t('mfa.subtitle')}</h2>
</div>
<!-- The actual form -->
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={tryMFACode}>
<div class="-space-y-px rounded-md shadow-sm">
<label for="mfa_token" class="sr-only">{t('mfa.prompt')}</label>
<input on:keypress={validateKeypress} bind:value={mfa_token} type="number" maxlength="6" id="mfa_token"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
{#if hasError}
<span class="text-red-600 text-sm">{error}</span>
{/if}
</div>
<button class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
{#if !isloading}
{t('mfa.actionButtonText')}
{:else}
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{/if}
</button>
</form>
{:else}
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{t('mfa.done')}</h1>
<h2 class="ftext-sm">{t('mfa.doneSubtitle')}</h2>
</div>
{/if}
</div>
</div>

View File

@ -1,152 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {get_user_info} from "$lib/auth";
import type {UserInfo} from "$lib/auth";
import {t} from "$lib/i18n";
import {startTotpSetup} from "$lib/totp";
import {browser} from "$app/environment";
import QR from "../../../components/QR.svelte";
import {finishTOTPSetup} from "$lib/totp";
import {setCookie} from "$lib/cookie";
let logger = new Logger("auth/mfasetup/+page.svelte");
logSetup();
let api_token = "";
let isLoadingMFA = true;
let hasError = false;
let error = "";
let totp_setup_token = "";
let totp_otpurl = "";
let totp_secret = "";
let mfa_token = "";
let isloading = false;
onMount(async () => {
let st_res = await enforce_session();
if (!st_res[0]) {
// session token is invalid
// Session token is invalid. redirect to mfa
logger.info("Invalid session token, redirecting to mfa");
window.location = "/auth/mfa";
return;
}
api_token = st_res[1];
let user: UserInfo | string = await get_user_info(api_token);
if (typeof user === "string") {
logger.error(`Unable to get user info: ${user}`);
hasError = true;
error = user;
return;
}
if (user.data.actor.hasTOTPAuthenticator) {
logger.info("User already has mfa set up, redirecting");
window.location = "/auth/mfa";
return;
}
let resp = await startTotpSetup(api_token);
if (typeof resp === "string") {
logger.error(`Unable to create TOTP token: ${resp}`);
hasError = true;
error = t(`mfasetup.apierror.${resp}`);
return;
}
totp_setup_token = resp.totpToken;
totp_secret = resp.secret;
totp_otpurl = resp.url;
isLoadingMFA = false;
});
function validateInput(e: KeyboardEvent) {
if (e.charCode === 13) { return; }
if (e.charCode < 47 || e.charCode > 57) {
e.preventDefault();
}
if (e.target.value.length >= 6) {
e.preventDefault();
}
}
async function tryMFACode() {
isloading = true;
logger.info(`Submitting 2FA verify with code ${mfa_token}`);
let resp = await finishTOTPSetup(api_token, totp_setup_token, mfa_token);
if (typeof resp === "string") {
logger.error(`Unable to validate TOTP token: ${resp}`);
hasError = true;
isloading = false;
error = t(`mfasetup.apierror.${resp}`);
return;
}
// set cookie
setCookie("authToken", resp.token);
window.location = "/admin";
window.location.reload();
}
</script>
<sveltekit:head>
<script src="https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs/qrcode.min.js"></script>
</sveltekit:head>
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<h1 class="font-semibold text-2xl">{t('mfasetup.title')}</h1>
<h2 class="text-sm">{t('mfasetup.subtitle')}</h2>
{#if isLoadingMFA}
<div class="mt-5">
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<h3 class="text-xs inline-block">{t('mfasetup.loadingmfa')}</h3>
</div>
{:else}
<h4 class="text-sm mt-5 font-semibold ">{t('mfasetup.qrtitle')}</h4>
<div class="mt-2 dark:bg-slate-600 bg-slate-200 rounded aspect-square p-4 w-min">
<QR codeValue="{totp_otpurl}"/>
</div>
<h4 class="text-sm mt-5">{t('mfasetup.secrettitle')}</h4>
<span class="font-mono text-md mt-4 block">
{totp_secret.match(/.{1,4}/g).join(" ")}
</span>
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={tryMFACode}>
<div class="-space-y-px rounded-md shadow-sm">
<input on:keypress={validateInput} bind:value={mfa_token} type="number" id="mfa_token" placeholder="{t('mfasetup.verifytitle')}"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
{#if hasError}
<span class="text-red-600 text-sm">{error}</span>
{/if}
</div>
<button class="-space-y-px bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 rounded-md shadow-sm place-content-center">
{#if !isloading}
{t('mfasetup.actionButtonText')}
{:else}
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{/if}
</button>
</form>
{/if}
</div>
</div>

View File

@ -1,86 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
let logger = new Logger("org/[orgid]/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
window.location.href = `/org/${orgid}/lighthouses`;
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
{/if}

View File

@ -1,86 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "$components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/hosts/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="hosts" orgid="{orgid}">
</OrgWrapper>
{/if}

View File

@ -1,86 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "../../../../components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/lighthouses/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="lighthouses" orgid="{orgid}">
</OrgWrapper>
{/if}

View File

@ -1,86 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "$components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/relays/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="relays" orgid="{orgid}">
</OrgWrapper>
{/if}

View File

@ -1,169 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "../../../../components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/roles/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
let roles = {
'data': []
};
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org/${orgid}/roles`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${api_token}`
}
});
if (!resp.ok) {
let err = JSON.parse(await resp.text()).errors[0].message;
logger.error(`${err}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadroles');
fullPageErrorSubtitle = t('org.apierror.' + err);
return;
}
roles = JSON.parse(await resp.text());
} catch (e) {
logger.error(`${e}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadroles');
fullPageErrorSubtitle = t('org.apierror.' + `${e}`.replaceAll('.', ''));
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="roles" orgid="{orgid}">
<div class="w-full">
<!-- Top bar -->
<h1 class="align-middle inline mt-10 font-semibold text-2xl">{t('org.roles_title')}</h1>
<button on:click={() => {window.location.href = `/org/${orgid}/roles/add`}} class="align-middle inline w-20 h-31 float-right dark:bg-purple-700 rounded font-bold text-sm p-2 w-25 border-2 dark:border-purple-600 border-purple-700 bg-purple-200">{t('org.roles_cta')}</button>
<!-- Infobox -->
<div class="mt-5 p-4 bg-purple-800/20 text-sm rounded">
<span>{t('org.roles_bar')}</span>
<a class="inline text-purple-600 hover:text-purple-700 transition font-semibold"
href="/docs/roles">{t('org.guide')}</a>
</div>
<!-- Table -->
<!-- roles.data.length === 0 -->
{#if roles.data.length === 0}
<div class="mt-5">
<h2 class="text-center text-xl font-semibold">{t('org.noroles')}</h2>
<h3 class="text-center text-md">{t('org.noroles_cta')}</h3>
</div>
{:else}
<div class="mt-5">
<table class="rounded w-full bg-white dark:bg-slate-800 text-sm shadow-sm">
<tr>
<th class="w-1/4 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">
Name
</th>
<th class="w-1/8 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">ID
</th>
<th class="w-1/8 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">
Rules
</th>
<th class="w-3/4 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">
Description
</th>
<th class="border-b p-4"></th>
</tr>
{#each roles.data as role}
<tr>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.name}</td>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.id}</td>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.firewall_rules.length}</td>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.description}</td>
<td>
<button class="w-16 border p-2 rounded m-2 hover:bg-slate-100 transition dark:hover:bg-slate-700" on:click={() => {
window.location.href = `/org/${orgid}/roles/${role.id}`
}}>
Edit
</button>
</td>
</tr>
{/each}
</table>
</div>
{/if}
</div>
</OrgWrapper>
{/if}

View File

@ -1,144 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "../../../../../components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/roles/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
let roles = {
'data': []
};
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
let name = "";
let description = "";
async function createRole() {
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org/${orgid}/role`, {
'method': 'POST',
'headers': {
'Authorization': `Bearer ${api_token}`
},
'body': JSON.stringify({
'name': name,
'description': description
})
});
if (!resp.ok) {
let err = JSON.parse(await resp.text()).errors[0].message;
logger.error(`${err}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.addrole');
fullPageErrorSubtitle = t('org.apierror.' + err);
return;
}
roles = JSON.parse(await resp.text());
window.location.href = `/org/${orgid}/roles`
} catch (e) {
logger.error(`${e}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadroles');
fullPageErrorSubtitle = t('org.apierror.' + `${e}`.replaceAll('.', ''));
return;
}
}
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<!-- The actual form -->
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={createRole}>
<div class="rounded-md shadow-sm">
<label for="name">{t('org.roleaddprompt')}</label>
<input bind:value={name} id="name"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
</div>
<div class="rounded-md shadow-sm mt-5">
<label for="desc">{t('org.roleaddpromptdesc')}</label>
<input bind:value={description} id="desc"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
</div>
<button class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
{t('org.roleadd_cta')}
</button>
</form>
</div>
</div>
{/if}

View File

@ -1,209 +0,0 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
let logger = new Logger("org/new/+page.svelte");
logSetup();
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
let org_ip_range = "172.16.0.0/24";
let org_subnets = "";
let org_groups = "";
let hasError_iprange = false;
let error_iprange = "";
let hasError_subnet = false;
let error_subnet = "";
let isloading = false;
let canSubmit = true;
function recalcCansubmit() {
canSubmit = hasError_iprange || fullPageError || hasError_subnet;
}
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let owned_org_id = user_info.data.actor.organizationID;
if (owned_org_id !== "") {
logger.error("User owns an org already");
window.location.href = "/admin";
}
})
let showAdditionalConstraints = false;
function handle(listfield: string): string[] {
if (listfield == "") {
return []
} else {
return listfield.split(',')
}
}
async function doCreateFlow() {
// STEP ONE: Create the org
logger.info("Creating organization");
let created_org_id;
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org`, {
'method': 'POST',
'headers': {
'Authorization': 'Bearer ' + api_token
},
'body': JSON.stringify({
'ip_ranges': handle(org_ip_range),
'subnet_ranges': handle(org_subnets),
'groups': handle(org_groups)
})
});
if (!resp.ok) {
if (resp.status === 422) {
// we had an invalid input
logger.error(`Invalid input`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.invaliddata');
return;
}
let text = await resp.text();
console.log(text);
let err = JSON.parse(text).errors[0].message;
logger.error(`${err}`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.' + err);
return;
}
let text = await resp.text();
console.log(text);
let resp_objectified = JSON.parse(text);
created_org_id = resp_objectified.org_id;
logger.info("Able to create organization with id " + created_org_id);
logger.info("Org create success! Redirecting to manage page");
window.location.href = "/org/" + created_org_id;
} catch (e) {
logger.error(`${e}`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.' + `${e}`.replaceAll('.', ''));
return;
}
}
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8 h-max">
<div class="w-full max-w-md m-auto">
<div>
<h1 class="font-semibold text-2xl">{t('neworg.title')}</h1>
<h2 class="text-sm">{t('neworg.subtitle')}</h2>
<h3 class="font-semibold mt-5">{t('neworg.firstcert')}</h3>
<h3 class="text-sm">{t('neworg.cahelp')}</h3>
</div>
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={doCreateFlow}>
<div class=" rounded-md shadow-sm">
<label for="iprange" class="mb-5">{t('neworg.iprangeprompt')}</label>
<input bind:value={org_ip_range} id="iprange"
class="mt-2 dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
<span class="text-sm block">{t('neworg.cidrhelp')}</span>
{#if hasError_iprange}
<span class="text-red-600 text-sm block">{error_iprange}</span>
{/if}
</div>
<label class="relative inline-flex items-center cursor-pointer mt-4 ">
<input bind:checked={showAdditionalConstraints} type="checkbox" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-purple-600"></div>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{t('neworg.additionalConstraints')}</span>
</label>
{#if showAdditionalConstraints}
<div class=" rounded-md shadow-sm">
<label for="subnets" class="mb-5">{t('neworg.subnetprompt')}</label>
<input bind:value={org_subnets} id="subnets"
class="mt-2 dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
<span class="text-sm block">{t('neworg.subnethelp')}</span>
{#if hasError_subnet}
<span class="text-red-600 text-sm block">{error_subnet}</span>
{/if}
</div>
<div class="mt-2 rounded-md shadow-sm">
<label for="groups" class="mb-5">{t('neworg.groupprompt')}</label>
<input bind:value={org_groups} id="groups"
class="mt-2 dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
<span class="text-sm block">{t('neworg.grouphelp')}</span>
{#if hasError_subnet}
<span class="text-red-600 text-sm block">{error_subnet}</span>
{/if}
</div>
{/if}
<button disabled="{!canSubmit}"
class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
{#if !isloading}
{t('neworg.actionButtonText')}
{:else}
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{/if}
</button>
</form>
</div>
</div>
{/if}

View File

@ -1,101 +0,0 @@
<script lang="ts">
import {t} from "$lib/i18n";
import {API_ROOT} from "$lib/config";
let email = "";
let isloading = false;
let isFinished = false;
let hasError = false;
let error = "";
function createAcc() {
if (isloading) {
return;
}
isloading = true;
let xhr = new XMLHttpRequest();
xhr.timeout = 10000;
xhr.open('POST', `${API_ROOT}/v1/signup`);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({
email: email
}));
xhr.ontimeout = () => {
hasError = true;
error = t('signup.apierror.timeout');
isloading = false;
};
xhr.onload = () => {
if (xhr.status != 200) {
// error
hasError = true;
const rawerror = JSON.parse(xhr.responseText).errors[0].message;
error = t(`signup.apierror.${rawerror}`);
isloading = false;
} else {
isloading = false;
isFinished = true;
}
};
xhr.onerror = () => {
hasError = true;
error = t('signup.apierror.xhrerror');
isloading = false;
};
}
</script>
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
{#if !isFinished}
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{t('signup.title')}</h1>
<h2 class="ftext-sm">{t('signup.subtitle')}</h2>
</div>
<!-- The actual form -->
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={createAcc}>
<div class="rounded-md shadow-sm">
<label for="email">{t('signup.prompt')}</label>
<input bind:value={email} id="email"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
{#if hasError}
<span class="text-red-600 text-sm">{error}</span>
{/if}
</div>
<button class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
{#if !isloading}
{t('signup.actionButtonText')}
{:else}
<svg class="animate-spin w-5 h-5 inline-block m-auto" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{/if}
</button>
<div class="text-xs mt-0.5">
<a class="font-bold text-purple-400 dark:text-purple-600" href="/auth/login">{t('signup.login')}</a>
</div>
</form>
{:else}
<!-- Title -->
<div>
<h1 class="font-semibold text-2xl">{t('signup.sentMagicLink')}</h1>
<h2 class="ftext-sm">{t('signup.magicLinkExplainer')}</h2>
</div>
{/if}
</div>
</div>

View File

@ -1,4 +1,4 @@
import adapter from '@sveltejs/adapter-static';
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
@ -8,13 +8,10 @@ const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: null,
precompress: false,
strict: true
})
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};

View File

@ -1,22 +0,0 @@
/** @type {import('tailwindcss').Config} */
const colors = require("tailwindcss/colors.js");
module.exports = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {
colors: {
neutral: colors.slate,
positive: colors.green,
urge: colors.violet,
warning: colors.yellow,
info: colors.blue,
critical: colors.red
}
},
},
plugins: [
require('a17t')
],
darkMode: 'class'
}

View File

@ -8,21 +8,7 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"paths": {
"$lib": [
"./src/lib"
],
"$lib/*": [
"./src/lib/*"
],
"$components": [
"./src/components"
],
"$components/*": [
"./src/components/*"
]
}
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//

File diff suppressed because it is too large Load Diff