merge milestone:feat-org-keys (workable)
This commit is contained in:
commit
8d45d2c6a8
|
@ -1022,6 +1022,9 @@ name = "ipnet"
|
|||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
|
@ -2403,6 +2406,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"dotenvy",
|
||||
"hex",
|
||||
"ipnet",
|
||||
"log",
|
||||
"paste",
|
||||
"rand",
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<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>
|
|
@ -92,6 +92,62 @@
|
|||
"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)"
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export async function fetch_timeout(resource: RequestInfo | URL, options = {}) {
|
||||
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;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
import {finishTOTPSetup} from "$lib/totp";
|
||||
import {setCookie} from "$lib/cookie";
|
||||
|
||||
let logger = new Logger("auth/mfa/+page.svelte");
|
||||
let logger = new Logger("auth/mfasetup/+page.svelte");
|
||||
logSetup();
|
||||
|
||||
let api_token = "";
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<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}
|
|
@ -0,0 +1,86 @@
|
|||
<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}
|
|
@ -0,0 +1,86 @@
|
|||
<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}
|
|
@ -0,0 +1,86 @@
|
|||
<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}
|
|
@ -0,0 +1,169 @@
|
|||
<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}
|
|
@ -0,0 +1,144 @@
|
|||
<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}
|
|
@ -8,8 +8,10 @@
|
|||
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("admin/+page.svelte");
|
||||
let logger = new Logger("org/new/+page.svelte");
|
||||
logSetup();
|
||||
|
||||
let api_token = "";
|
||||
|
@ -73,8 +75,65 @@
|
|||
|
||||
let showAdditionalConstraints = false;
|
||||
|
||||
function newOrg() {
|
||||
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>
|
||||
|
||||
|
@ -90,7 +149,7 @@
|
|||
<h3 class="text-sm">{t('neworg.cahelp')}</h3>
|
||||
</div>
|
||||
|
||||
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={newOrg}>
|
||||
<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"
|
||||
|
|
|
@ -8,7 +8,21 @@
|
|||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"$lib": [
|
||||
"./src/lib"
|
||||
],
|
||||
"$lib/*": [
|
||||
"./src/lib/*"
|
||||
],
|
||||
"$components": [
|
||||
"./src/components"
|
||||
],
|
||||
"$components/*": [
|
||||
"./src/components/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
|
|
|
@ -25,3 +25,4 @@ hex = "0.4.3"
|
|||
rand = "0.8.5"
|
||||
trifid-pki = { version = "0.1.3", path = "../trifid-pki" }
|
||||
sha2 = "0.10.6"
|
||||
ipnet = { version = "2.7.1", features = ["serde"] }
|
||||
|
|
|
@ -72,3 +72,10 @@ totp_verification_valid_for = 3600
|
|||
# Do not change this value in a production instance. It will make existing data inaccessible until changed back.
|
||||
# ------- WARNING -------
|
||||
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
|
||||
|
||||
# How long should CA certs be valid for before they need to be replaced (in seconds)?
|
||||
# This controls the maximum amount of time a network on this instance can go
|
||||
# without a rekey.
|
||||
# You probably don't need to change, this, 31536000 (1 year) is a sane default.
|
||||
# This value only affects new certs signed by this instance.
|
||||
ca_certs_valid_for = 31536000
|
|
@ -6,3 +6,4 @@ magic_links_valid_for = 86400
|
|||
session_tokens_valid_for = 86400
|
||||
totp_verification_valid_for = 3600
|
||||
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
|
||||
ca_certs_valid_for = 31536000
|
|
@ -0,0 +1,22 @@
|
|||
-- 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/>.
|
||||
|
||||
CREATE TABLE roles (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
org SERIAL NOT NULL REFERENCES organizations(id),
|
||||
name VARCHAR(128) NOT NULL,
|
||||
description VARCHAR(4096) NOT NULL
|
||||
);
|
|
@ -0,0 +1,27 @@
|
|||
-- 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/>.
|
||||
|
||||
CREATE TABLE roles_firewall_rules (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
role SERIAL NOT NULL REFERENCES roles(id),
|
||||
protocol INTEGER NOT NULL, -- 0: any 1: tcp 2: udp 3: icmp
|
||||
port_range_start INTEGER NOT NULL, -- min: 1 max: 65535. Ignored if protocol==3
|
||||
port_range_end INTEGER NOT NULL, -- min: 1 max: 65535, must be greater than or equal to port_range_start. Ignored if protocol==3
|
||||
allow_from INTEGER NOT NULL, -- Allow traffic goverened by above rules from who?
|
||||
-- -1: anybody
|
||||
-- (a role, anything else): only that role
|
||||
description VARCHAR(4096) NOT NULL
|
||||
);
|
|
@ -23,8 +23,9 @@ pub struct TFConfig {
|
|||
pub db_url: String,
|
||||
pub base: Url,
|
||||
pub web_root: Url,
|
||||
pub magic_links_valid_for: i64,
|
||||
pub session_tokens_valid_for: i64,
|
||||
pub totp_verification_valid_for: i64,
|
||||
pub data_key: String
|
||||
pub magic_links_valid_for: u64,
|
||||
pub session_tokens_valid_for: u64,
|
||||
pub totp_verification_valid_for: u64,
|
||||
pub data_key: String,
|
||||
pub ca_certs_valid_for: u64
|
||||
}
|
|
@ -41,6 +41,7 @@ pub mod auth;
|
|||
pub mod crypto;
|
||||
pub mod org;
|
||||
pub mod kv;
|
||||
pub mod role;
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!();
|
||||
|
||||
|
@ -180,7 +181,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
crate::routes::v1::organization::orglist_req,
|
||||
crate::routes::v1::organization::create_org,
|
||||
crate::routes::v1::user::get_user,
|
||||
crate::routes::v1::user::options
|
||||
crate::routes::v1::user::options,
|
||||
crate::routes::v1::organization::createorgoptions,
|
||||
crate::routes::v1::ca::get_cas_for_org,
|
||||
crate::routes::v1::ca::options,
|
||||
crate::routes::v1::roles::get_roles,
|
||||
crate::routes::v1::roles::options,
|
||||
crate::routes::v1::roles::options_roleadd,
|
||||
crate::routes::v1::roles::role_add
|
||||
])
|
||||
.register("/", catchers![
|
||||
crate::routes::handler_400,
|
||||
|
|
|
@ -15,11 +15,13 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use rocket::form::validate::Contains;
|
||||
use sqlx::PgPool;
|
||||
use trifid_pki::ca::NebulaCAPool;
|
||||
|
||||
pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result<Option<i32>, Box<dyn Error>> {
|
||||
|
||||
Ok(sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await?.map(|r| r.id))
|
||||
}
|
||||
|
||||
|
@ -60,7 +62,9 @@ pub async fn get_associated_orgs(user: i32, db: &PgPool) -> Result<Vec<i32>, Box
|
|||
}
|
||||
|
||||
pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
|
||||
Ok(get_associated_orgs(user, db).await?.contains(org))
|
||||
let associated_orgs = get_associated_orgs(user, db).await?;
|
||||
|
||||
Ok(associated_orgs.contains(org))
|
||||
}
|
||||
|
||||
pub async fn get_org_ca_pool(org: i32, db: &PgPool) -> Result<NebulaCAPool, Box<dyn Error>> {
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// 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 std::error::Error;
|
||||
use sqlx::PgPool;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[repr(i32)]
|
||||
pub enum Protocol {
|
||||
Any = 0,
|
||||
TCP = 1,
|
||||
UDP = 2,
|
||||
ICMP = 3
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AllowFrom {
|
||||
Anyone,
|
||||
SpecificRole(i32)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FirewallRule {
|
||||
pub id: i32,
|
||||
pub protocol: Protocol,
|
||||
pub port_start: u16,
|
||||
pub port_end: u16,
|
||||
pub allow_from: AllowFrom,
|
||||
pub description: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Role {
|
||||
pub id: i32,
|
||||
pub org_id: i32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub firewall_rules: Vec<FirewallRule>
|
||||
}
|
||||
|
||||
pub async fn get_role(role_id: i32, db: &PgPool) -> Result<Option<Role>, Box<dyn Error>> {
|
||||
let query_result: Option<_> = sqlx::query!("SELECT * FROM roles WHERE id = $1", role_id).fetch_optional(db).await?;
|
||||
|
||||
if let Some(res) = query_result {
|
||||
// get all firewall rules
|
||||
let rules_res = sqlx::query!("SELECT * FROM roles_firewall_rules WHERE role = $1", Some(role_id)).fetch_all(db).await?;
|
||||
|
||||
let mut rules = vec![];
|
||||
|
||||
for rule in rules_res {
|
||||
rules.push(FirewallRule {
|
||||
id: rule.id,
|
||||
protocol: match rule.protocol {
|
||||
0 => Protocol::Any,
|
||||
1 => Protocol::TCP,
|
||||
2 => Protocol::UDP,
|
||||
3 => Protocol::ICMP,
|
||||
_ => return Err(format!("invalid protocol on a firewall rule {}", rule.id).into())
|
||||
},
|
||||
port_start: rule.port_range_start as u16,
|
||||
port_end: rule.port_range_end as u16,
|
||||
allow_from: match rule.allow_from {
|
||||
-1 => AllowFrom::Anyone,
|
||||
_ => AllowFrom::SpecificRole(rule.allow_from)
|
||||
},
|
||||
description: rule.description,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(Some(Role {
|
||||
id: role_id,
|
||||
org_id: res.org,
|
||||
name: res.name,
|
||||
description: res.description,
|
||||
firewall_rules: rules,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_role_ids_for_ca(org_id: i32, db: &PgPool) -> Result<Vec<i32>, Box<dyn Error>> {
|
||||
Ok(sqlx::query!("SELECT id FROM roles WHERE org = $1", org_id).fetch_all(db).await?.iter().map(|r| r.id).collect())
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// 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 rocket::{options, get, State};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::serde::json::Json;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::auth::TOTPAuthenticatedUserInfo;
|
||||
use crate::org::{get_associated_orgs, get_org_ca_pool};
|
||||
|
||||
#[options("/v1/org/<_id>/ca")]
|
||||
pub fn options(_id: i32) -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CaList {
|
||||
pub trusted_cas: Vec<CA>,
|
||||
pub blocklisted_certs: Vec<String>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CA {
|
||||
pub fingerprint: String,
|
||||
pub cert: String
|
||||
}
|
||||
|
||||
#[get("/v1/org/<id>/ca")]
|
||||
pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<CaList>), (Status, String)> {
|
||||
let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query", e)))
|
||||
};
|
||||
|
||||
if !associated_orgs.contains(&id) {
|
||||
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_NOT_YOUR_ORG", "you are not authorized to view details of this org")))
|
||||
}
|
||||
|
||||
let ca_pool = match get_org_ca_pool(id, db.inner()).await {
|
||||
Ok(pool) => pool,
|
||||
Err(e) => {
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to load certificates from database - {}\"}}]}}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
let mut trusted_cas = vec![];
|
||||
|
||||
for (fingerprint, cert) in ca_pool.cas {
|
||||
trusted_cas.push(CA {
|
||||
fingerprint,
|
||||
cert: match cert.serialize_to_pem() {
|
||||
Ok(pem) => match String::from_utf8(pem) {
|
||||
Ok(str) => str,
|
||||
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to encode one of the serialized certificates - {}\"}}]}}", e)))
|
||||
},
|
||||
Err(e) => {
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to serialize one of the certificates - {}\"}}]}}", e)));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Ok((ContentType::JSON, Json(CaList {
|
||||
trusted_cas,
|
||||
blocklisted_certs: ca_pool.cert_blocklist,
|
||||
})))
|
||||
}
|
|
@ -21,3 +21,5 @@ pub mod totp_authenticators;
|
|||
pub mod verify_totp_authenticator;
|
||||
pub mod organization;
|
||||
pub mod user;
|
||||
pub mod ca;
|
||||
pub mod roles;
|
|
@ -14,11 +14,16 @@
|
|||
// 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 rocket::{get, post, options, State};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use ipnet::Ipv4Net;
|
||||
use rocket::{get, post, options, State, error};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::PgPool;
|
||||
use trifid_pki::cert::{NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private};
|
||||
use trifid_pki::ed25519_dalek::{SigningKey};
|
||||
use trifid_pki::rand_core::OsRng;
|
||||
use crate::auth::TOTPAuthenticatedUserInfo;
|
||||
use crate::config::TFConfig;
|
||||
use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config};
|
||||
|
@ -32,6 +37,8 @@ pub fn options() -> &'static str {
|
|||
pub fn orgidoptions(_id: i32) -> &'static str {
|
||||
""
|
||||
}
|
||||
#[options("/v1/org")]
|
||||
pub fn createorgoptions() -> &'static str {""}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
|
@ -64,7 +71,7 @@ pub struct OrginfoStruct {
|
|||
|
||||
#[get("/v1/org/<orgid>")]
|
||||
pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
|
||||
if !user_has_org_assoc(orgid, user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
|
||||
if !user_has_org_assoc(user.user_id, orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
|
||||
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
|
||||
}
|
||||
|
||||
|
@ -81,12 +88,59 @@ pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State
|
|||
)))
|
||||
}
|
||||
|
||||
#[post("/v1/org")]
|
||||
pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CreateCARequest {
|
||||
pub ip_ranges: Vec<Ipv4Net>,
|
||||
pub subnet_ranges: Vec<Ipv4Net>,
|
||||
pub groups: Vec<String>
|
||||
}
|
||||
|
||||
#[post("/v1/org", data = "<req>")]
|
||||
pub async fn create_org(req: Json<CreateCARequest>, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
|
||||
if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?.is_some() {
|
||||
return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time")))
|
||||
}
|
||||
|
||||
// Generate the CA keypair
|
||||
let private_key = SigningKey::generate(&mut OsRng);
|
||||
let public_key = private_key.verifying_key();
|
||||
|
||||
// Create the CA certificate
|
||||
let mut ca_cert = NebulaCertificate {
|
||||
details: NebulaCertificateDetails {
|
||||
name: format!("{}'s Organization - Root Signing CA", user.email),
|
||||
ips: req.ip_ranges.clone(),
|
||||
subnets: req.subnet_ranges.clone(),
|
||||
groups: req.groups.clone(),
|
||||
not_before: SystemTime::now(),
|
||||
not_after: SystemTime::now() + Duration::from_secs(config.ca_certs_valid_for),
|
||||
public_key: public_key.to_bytes(),
|
||||
is_ca: true,
|
||||
issuer: "".to_string(), // This is a self-signed certificate! There is no issuer present
|
||||
},
|
||||
signature: vec![],
|
||||
};
|
||||
// Self-sign the CA certificate
|
||||
match ca_cert.sign(&private_key) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
error!("[tfapi] security: certificate signature error: {}", e);
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_SIGN_ERROR", "there was an error generating the CA certificate, please try again later")))
|
||||
}
|
||||
}
|
||||
|
||||
// PEM-encode the CA key
|
||||
let ca_key_pem = serialize_ed25519_private(&private_key.to_keypair_bytes());
|
||||
// PEM-encode the CA cert
|
||||
let ca_cert_pem = match ca_cert.serialize_to_pem() {
|
||||
Ok(pem) => pem,
|
||||
Err(e) => {
|
||||
error!("[tfapi] security: certificate encoding error: {}", e);
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_ENCODE_ERROR", "there was an error encoding the CA certificate, please try again later")))
|
||||
}
|
||||
};
|
||||
|
||||
// generate an AES iv to use for key encryption
|
||||
let iv = generate_random_iv();
|
||||
let iv_hex = hex::encode(iv);
|
||||
|
@ -98,16 +152,33 @@ pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, con
|
|||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e)));
|
||||
}
|
||||
};
|
||||
let ca_key = match encrypt_with_nonce(b"", iv, &cipher) {
|
||||
let ca_key = match encrypt_with_nonce(&ca_key_pem, iv, &cipher) {
|
||||
Ok(key) => hex::encode(key),
|
||||
Err(e) => {
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e)));
|
||||
}
|
||||
};
|
||||
let ca_crt = hex::encode(b"");
|
||||
let ca_crt = hex::encode(ca_cert_pem);
|
||||
|
||||
let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;
|
||||
|
||||
// last step: create a default role to allow pings from all hosts
|
||||
let role_id = match sqlx::query!("INSERT INTO roles (org, name, description) VALUES ($1, 'Default', 'Allow pings from other hosts. Default role for new hosts.') RETURNING id", result.id).fetch_one(db.inner()).await {
|
||||
Ok(r) => r.id,
|
||||
Err(e) => {
|
||||
error!("[tfapi] dberror: {}", e);
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role")));
|
||||
}
|
||||
};
|
||||
|
||||
match sqlx::query!("INSERT INTO roles_firewall_rules (role, protocol, port_range_start, port_range_end, allow_from, description) VALUES ($1, 3, 1, 1, -1, 'Allow pings from anyone on the network')", role_id).execute(db.inner()).await {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
error!("[tfapi] dberror: {} inserting on roleid {}", e, role_id);
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role firewall rules")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((ContentType::JSON, Json(
|
||||
OrginfoStruct {
|
||||
org_id: result.id,
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
// 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 log::error;
|
||||
use rocket::{options, get, State, post};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::PgPool;
|
||||
use crate::auth::TOTPAuthenticatedUserInfo;
|
||||
use crate::org::user_has_org_assoc;
|
||||
use crate::role::{get_role, get_role_ids_for_ca, Role};
|
||||
|
||||
#[options("/v1/org/<_org>/roles")]
|
||||
pub fn options(_org: i32) -> &'static str { "" }
|
||||
#[options("/v1/org/<_org>/role")]
|
||||
pub fn options_roleadd(_org: i32) -> &'static str { "" }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct RolesResponse {
|
||||
pub data: Vec<Role>
|
||||
}
|
||||
|
||||
#[get("/v1/org/<org>/roles")]
|
||||
pub async fn get_roles(org: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<RolesResponse>), (Status, String)> {
|
||||
if !user_has_org_assoc(user.user_id, org, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
|
||||
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
|
||||
}
|
||||
|
||||
let roles = match get_role_ids_for_ca(org, db.inner()).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", e)))
|
||||
}
|
||||
};
|
||||
|
||||
let mut resp = RolesResponse {
|
||||
data: vec![]
|
||||
};
|
||||
|
||||
for role in roles {
|
||||
let role_info = match get_role(role, db.inner()).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", e)))
|
||||
}
|
||||
};
|
||||
if let Some(info) = role_info {
|
||||
resp.data.push(info);
|
||||
} else {
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", "missing role as returned by server - possibly deleted inbetween?")))
|
||||
}
|
||||
}
|
||||
|
||||
Ok((ContentType::JSON, Json(resp)))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct RoleaddReq {
|
||||
pub name: String,
|
||||
pub description: String
|
||||
}
|
||||
|
||||
#[post("/v1/org/<org>/role", data = "<req>")]
|
||||
pub async fn role_add(req: Json<RoleaddReq>, org: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, String), (Status, String)> {
|
||||
if !user_has_org_assoc(user.user_id, org, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
|
||||
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
|
||||
}
|
||||
|
||||
match sqlx::query!("INSERT INTO roles (org, name, description) VALUES ($1, $2, $3)", org, req.name, req.description).execute(db.inner()).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
error!("[tfapi] dberror: {}", e);
|
||||
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "database returned error while trying to create role", "unable to insert role")))
|
||||
}
|
||||
}
|
||||
|
||||
Ok((ContentType::JSON, "{}".to_string()))
|
||||
}
|
|
@ -74,7 +74,7 @@ pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) ->
|
|||
|
||||
let otpid = format!("totp-{}", Uuid::new_v4());
|
||||
|
||||
sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;
|
||||
sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;
|
||||
|
||||
Ok((otpid, totpmachine))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue