webui skeleton
This commit is contained in:
parent
bce97ffe0b
commit
36c0bce365
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -8,4 +8,4 @@ node_modules
|
|||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
src/lib/config.ts
|
||||
.idea
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
engine-strict=true
|
||||
resolution-mode=highest
|
||||
|
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let tone = "~neutral";
|
||||
export let priority = "";
|
||||
</script>
|
||||
|
||||
<span class="badge {tone} {priority}"><slot/></span>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
// Set this to the API root of your trifid-api instance.
|
||||
export const API_ROOT = "http://localhost:8000";
|
|
@ -1,2 +0,0 @@
|
|||
// Set this to the API root of your trifid-api instance.
|
||||
export const API_ROOT = "http://localhost:8000";
|
|
@ -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 "";
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
|
@ -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": "ユーザーが存在しません。アカウントの作成を検討してください。"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import { persist } from "$lib/PersistentStore";
|
||||
|
||||
export const locale = persist("lang", "en");
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)}"/>
|
|
@ -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 />
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 →</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 →</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 →</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}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
|
@ -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
|
||||
//
|
||||
|
|
1179
tfweb/yarn.lock
1179
tfweb/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue