UI/org work
This commit is contained in:
parent
7a074bf38f
commit
58be3b0c4a
|
@ -0,0 +1,14 @@
|
||||||
|
<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>
|
|
@ -7,6 +7,10 @@
|
||||||
import {getCookie} from "$lib/cookie";
|
import {getCookie} from "$lib/cookie";
|
||||||
import {setCookie} from "$lib/cookie";
|
import {setCookie} from "$lib/cookie";
|
||||||
import {get_user_info, renderDevMenu} from "$lib/auth";
|
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() {
|
function toggleTheme() {
|
||||||
if ($theme === "dark") {
|
if ($theme === "dark") {
|
||||||
|
@ -50,7 +54,13 @@
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<nav class="flex items-center justify-between flex-wrap slate-300 dark:bg-slate-800 p-6 shadow-md">
|
<nav class="flex items-center justify-between flex-wrap slate-300 dark:bg-slate-800 p-6 shadow-md">
|
||||||
<span class="font-semibold text-xl tracking-tight">{t("common.appName")}</span>
|
<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="inline-block text-sm leading-none lg:m-0">
|
||||||
<div class="relative inline-block">
|
<div class="relative inline-block">
|
||||||
|
|
|
@ -123,6 +123,9 @@ export async function enforce_auth(): Promise<[boolean, string]> {
|
||||||
} else {
|
} else {
|
||||||
// session ok
|
// session ok
|
||||||
logger.info("MFA token is OK");
|
logger.info("MFA token is OK");
|
||||||
|
if (browser) {
|
||||||
|
document.documentElement.classList.add("loggedin");
|
||||||
|
}
|
||||||
return [true, `${session_token} ${auth_token}`];
|
return [true, `${session_token} ${auth_token}`];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -69,5 +69,29 @@
|
||||||
"apierror": {
|
"apierror": {
|
||||||
"Invalid TOTP code": "Incorrect 2FA code"
|
"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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<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,31 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import {enforce_auth, enforce_session} from "../../lib/auth";
|
import {enforce_auth, enforce_session} from "$lib/auth";
|
||||||
import {Logger, logSetup} from "../../lib/logger";
|
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");
|
let logger = new Logger("admin/+page.svelte");
|
||||||
logSetup();
|
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.
|
// this page requires session and mfa auth.
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let st_result = await enforce_session();
|
let st_result = await enforce_session();
|
||||||
|
@ -20,8 +40,128 @@
|
||||||
window.location = "/auth/mfa";
|
window.location = "/auth/mfa";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const api_token = at_result[1];
|
|
||||||
|
|
||||||
// user is fully authenticated and permitted to proceed
|
// 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");
|
||||||
})
|
})
|
||||||
</script>
|
|
||||||
|
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}
|
|
@ -73,8 +73,8 @@
|
||||||
|
|
||||||
<!-- The actual form -->
|
<!-- The actual form -->
|
||||||
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={generateMagicLink}>
|
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={generateMagicLink}>
|
||||||
<div class="-space-y-px rounded-md shadow-sm">
|
<div class="rounded-md shadow-sm">
|
||||||
<label for="email" class="sr-only">{t('login.prompt')}</label>
|
<label for="email">{t('login.prompt')}</label>
|
||||||
<input bind:value={email} id="email"
|
<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">
|
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}
|
{#if hasError}
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
<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 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 newOrg() {
|
||||||
|
|
||||||
|
}
|
||||||
|
</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={newOrg}>
|
||||||
|
<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}
|
|
@ -62,8 +62,8 @@
|
||||||
|
|
||||||
<!-- The actual form -->
|
<!-- The actual form -->
|
||||||
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={createAcc}>
|
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={createAcc}>
|
||||||
<div class="-space-y-px rounded-md shadow-sm">
|
<div class="rounded-md shadow-sm">
|
||||||
<label for="email" class="sr-only">{t('signup.prompt')}</label>
|
<label for="email">{t('signup.prompt')}</label>
|
||||||
<input bind:value={email} id="email"
|
<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">
|
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}
|
{#if hasError}
|
||||||
|
|
|
@ -178,7 +178,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
crate::routes::v1::organization::orgidoptions,
|
crate::routes::v1::organization::orgidoptions,
|
||||||
crate::routes::v1::organization::orginfo_req,
|
crate::routes::v1::organization::orginfo_req,
|
||||||
crate::routes::v1::organization::orglist_req,
|
crate::routes::v1::organization::orglist_req,
|
||||||
crate::routes::v1::organization::create_org
|
crate::routes::v1::organization::create_org,
|
||||||
|
crate::routes::v1::user::get_user,
|
||||||
|
crate::routes::v1::user::options
|
||||||
])
|
])
|
||||||
.register("/", catchers![
|
.register("/", catchers![
|
||||||
crate::routes::handler_400,
|
crate::routes::handler_400,
|
||||||
|
|
|
@ -19,4 +19,5 @@ pub mod signup;
|
||||||
|
|
||||||
pub mod totp_authenticators;
|
pub mod totp_authenticators;
|
||||||
pub mod verify_totp_authenticator;
|
pub mod verify_totp_authenticator;
|
||||||
pub mod organization;
|
pub mod organization;
|
||||||
|
pub mod user;
|
|
@ -0,0 +1,47 @@
|
||||||
|
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
|
||||||
|
// Copyright (C) 2023 c0repwn3r
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use rocket::{get, options};
|
||||||
|
use rocket::http::{ContentType, Status};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::State;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
#[options("/v1/user/<_id>")]
|
||||||
|
pub fn options(_id: i32) -> &'static str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct UserInfoResponse {
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/v1/user/<id>")]
|
||||||
|
pub async fn get_user(id: i32, db: &State<PgPool>) -> Result<(ContentType, Json<UserInfoResponse>), (Status, String)> {
|
||||||
|
let user = match sqlx::query!("SELECT email FROM users WHERE id = $1", id.clone() as i32).fetch_one(db.inner()).await {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((ContentType::JSON, Json(
|
||||||
|
UserInfoResponse {
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ use rocket::http::{ContentType, Status};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use crate::auth::PartialUserInfo;
|
use crate::auth::PartialUserInfo;
|
||||||
|
use crate::org::get_org_by_owner_id;
|
||||||
use crate::tokens::user_has_totp;
|
use crate::tokens::user_has_totp;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -58,12 +59,19 @@ pub fn options() -> &'static str {
|
||||||
|
|
||||||
#[get("/v2/whoami")]
|
#[get("/v2/whoami")]
|
||||||
pub async fn whoami_request(user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<WhoamiResponse>), (Status, String)> {
|
pub async fn whoami_request(user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<WhoamiResponse>), (Status, String)> {
|
||||||
|
let org = match get_org_by_owner_id(user.user_id, db.inner()).await {
|
||||||
|
Ok(b) => match b {
|
||||||
|
Some(r) => r.to_string(),
|
||||||
|
None => String::new()
|
||||||
|
},
|
||||||
|
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DBERROR", "an error occured trying to verify your user", e)))
|
||||||
|
};
|
||||||
Ok((ContentType::JSON, Json(WhoamiResponse {
|
Ok((ContentType::JSON, Json(WhoamiResponse {
|
||||||
data: WhoamiData {
|
data: WhoamiData {
|
||||||
actor_type: "user".to_string(),
|
actor_type: "user".to_string(),
|
||||||
actor: WhoamiActor {
|
actor: WhoamiActor {
|
||||||
id: user.user_id.to_string(),
|
id: user.user_id.to_string(),
|
||||||
organization_id: "TEMP_ORG_BECAUSE_THAT_ISNT_IMPLEMENTED_YET".to_string(),
|
organization_id: org,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
created_at: NaiveDateTime::from_timestamp_opt(user.created_at, 0).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339(),
|
created_at: NaiveDateTime::from_timestamp_opt(user.created_at, 0).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339(),
|
||||||
has_totpauthenticator: match user_has_totp(user.user_id, db.inner()).await {
|
has_totpauthenticator: match user_has_totp(user.user_id, db.inner()).await {
|
||||||
|
|
Loading…
Reference in New Issue