diff --git a/tfweb/src/lib/auth.ts b/tfweb/src/lib/auth.ts
index 4925384..4d21b49 100644
--- a/tfweb/src/lib/auth.ts
+++ b/tfweb/src/lib/auth.ts
@@ -123,6 +123,9 @@ export async function enforce_auth(): Promise<[boolean, string]> {
} else {
// session ok
logger.info("MFA token is OK");
+ if (browser) {
+ document.documentElement.classList.add("loggedin");
+ }
return [true, `${session_token} ${auth_token}`];
}
} catch (e) {
diff --git a/tfweb/src/lib/i18n/en.json b/tfweb/src/lib/i18n/en.json
index 77496a8..74cda5c 100644
--- a/tfweb/src/lib/i18n/en.json
+++ b/tfweb/src/lib/i18n/en.json
@@ -69,5 +69,29 @@
"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)"
}
}
\ No newline at end of file
diff --git a/tfweb/src/lib/orgs.ts b/tfweb/src/lib/orgs.ts
new file mode 100644
index 0000000..ab1cf39
--- /dev/null
+++ b/tfweb/src/lib/orgs.ts
@@ -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
{
+ 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 {
+ 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;
+}
\ No newline at end of file
diff --git a/tfweb/src/routes/+error.svelte b/tfweb/src/routes/+error.svelte
new file mode 100644
index 0000000..304aedc
--- /dev/null
+++ b/tfweb/src/routes/+error.svelte
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/tfweb/src/routes/admin/+page.svelte b/tfweb/src/routes/admin/+page.svelte
index b11f758..e842f44 100644
--- a/tfweb/src/routes/admin/+page.svelte
+++ b/tfweb/src/routes/admin/+page.svelte
@@ -1,11 +1,31 @@
\ No newline at end of file
+
+ function setOrg(to) {
+ window.location.href = "/org/" + to;
+ }
+
+ function newOrg() {
+ window.location.href = "/org/new";
+ }
+
+ function joinOrg() {
+ window.location.href = "/org/join";
+ }
+
+
+{#if fullPageError}
+
+{:else}
+
+
+
+
Select an organization to manage
+
+
+
+ {#if owns_org}
+
{setOrg(owned_org_id)}}>
+ Your organization
+ Next →
+
+ {:else}
+
{newOrg()}}>
+ + Create an organization
+
+ {/if}
+
+
+
+
+
{joinOrg()}}>
+ + Join an existing organization
+
+
+
+
+
+
+
+{/if}
\ No newline at end of file
diff --git a/tfweb/src/routes/auth/login/+page.svelte b/tfweb/src/routes/auth/login/+page.svelte
index 13f6cf5..f344cba 100644
--- a/tfweb/src/routes/auth/login/+page.svelte
+++ b/tfweb/src/routes/auth/login/+page.svelte
@@ -73,8 +73,8 @@