diff --git a/config.example.yml b/config.example.yml
index d0cbcec5..c3179941 100644
--- a/config.example.yml
+++ b/config.example.yml
@@ -37,5 +37,12 @@ email:
smtp_pass: aaaaaaaaaaaaaaaaaa
no_reply: no-reply@example.io
+users:
+ server_admin:
+ email: admin@example.com
+ password: Password123!
+
flags:
require_email_verification: true
+ disable_signup_without_invite: true
+ disable_user_create_org: true
diff --git a/server/auth/checkValidInvite.ts b/server/auth/checkValidInvite.ts
new file mode 100644
index 00000000..0965b590
--- /dev/null
+++ b/server/auth/checkValidInvite.ts
@@ -0,0 +1,42 @@
+import db from "@server/db";
+import { UserInvite, userInvites } from "@server/db/schema";
+import { isWithinExpirationDate } from "oslo";
+import { verifyPassword } from "./password";
+import { eq } from "drizzle-orm";
+
+export async function checkValidInvite({
+ inviteId,
+ token
+}: {
+ inviteId: string;
+ token: string;
+}): Promise<{ error?: string; existingInvite?: UserInvite }> {
+ const existingInvite = await db
+ .select()
+ .from(userInvites)
+ .where(eq(userInvites.inviteId, inviteId))
+ .limit(1);
+
+ if (!existingInvite.length) {
+ return {
+ error: "Invite ID or token is invalid"
+ };
+ }
+
+ if (!isWithinExpirationDate(new Date(existingInvite[0].expiresAt))) {
+ return {
+ error: "Invite has expired"
+ };
+ }
+
+ const validToken = await verifyPassword(token, existingInvite[0].tokenHash);
+ if (!validToken) {
+ return {
+ error: "Invite ID or token is invalid"
+ };
+ }
+
+ return {
+ existingInvite: existingInvite[0]
+ };
+}
diff --git a/server/config.ts b/server/config.ts
index 8d78cbc0..13e6c14f 100644
--- a/server/config.ts
+++ b/server/config.ts
@@ -62,12 +62,17 @@ const environmentSchema = z.object({
no_reply: z.string().email().optional()
})
.optional(),
+ users: z.object({
+ server_admin: z.object({
+ email: z.string().email(),
+ password: z.string()
+ })
+ }),
flags: z
.object({
- allow_org_subdomain_changing: z.boolean().optional(),
require_email_verification: z.boolean().optional(),
disable_signup_without_invite: z.boolean().optional(),
- require_signup_secret: z.boolean().optional()
+ disable_user_create_org: z.boolean().optional()
})
.optional()
});
@@ -156,5 +161,13 @@ process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name;
process.env.RESOURCE_SESSION_COOKIE_NAME =
parsedConfig.data.server.resource_session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
+process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
+ ?.disable_signup_without_invite
+ ? "true"
+ : "false";
+process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
+ ?.disable_user_create_org
+ ? "true"
+ : "false";
export default parsedConfig.data;
diff --git a/server/db/schema.ts b/server/db/schema.ts
index 68b1ebe0..190ad7f3 100644
--- a/server/db/schema.ts
+++ b/server/db/schema.ts
@@ -89,7 +89,10 @@ export const users = sqliteTable("user", {
emailVerified: integer("emailVerified", { mode: "boolean" })
.notNull()
.default(false),
- dateCreated: text("dateCreated").notNull()
+ dateCreated: text("dateCreated").notNull(),
+ serverAdmin: integer("serverAdmin", { mode: "boolean" })
+ .notNull()
+ .default(false)
});
export const newts = sqliteTable("newt", {
diff --git a/server/emails/templates/VerifyEmailCode.tsx b/server/emails/templates/VerifyEmailCode.tsx
index fc8978ed..9adab19b 100644
--- a/server/emails/templates/VerifyEmailCode.tsx
+++ b/server/emails/templates/VerifyEmailCode.tsx
@@ -7,7 +7,7 @@ import {
Preview,
Section,
Text,
- Tailwind,
+ Tailwind
} from "@react-email/components";
import * as React from "react";
@@ -20,7 +20,7 @@ interface VerifyEmailProps {
export const VerifyEmail = ({
username,
verificationCode,
- verifyLink,
+ verifyLink
}: VerifyEmailProps) => {
const previewText = `Verify your email, ${username}`;
@@ -33,10 +33,10 @@ export const VerifyEmail = ({
theme: {
extend: {
colors: {
- primary: "#F97317",
- },
- },
- },
+ primary: "#F97317"
+ }
+ }
+ }
}}
>
@@ -48,11 +48,8 @@ export const VerifyEmail = ({
Hi {username || "there"},
- You’ve requested to verify your email. Please{" "}
-
- click here
- {" "}
- to verify your email, then enter the following code:
+ You’ve requested to verify your email. Please use
+ the code below to complete the verification process upon logging in.
diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts
index 0dbf2f4a..10815da3 100644
--- a/server/routers/auth/signup.ts
+++ b/server/routers/auth/signup.ts
@@ -16,16 +16,19 @@ import {
createSession,
generateId,
generateSessionToken,
- serializeSessionCookie,
+ serializeSessionCookie
} from "@server/auth";
import { ActionsEnum } from "@server/auth/actions";
import config from "@server/config";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
+import { checkValidInvite } from "@server/auth/checkValidInvite";
export const signupBodySchema = z.object({
email: z.string().email(),
password: passwordSchema,
+ inviteToken: z.string().optional(),
+ inviteId: z.string().optional()
});
export type SignUpBody = z.infer;
@@ -50,11 +53,39 @@ export async function signup(
);
}
- const { email, password } = parsedBody.data;
+ const { email, password, inviteToken, inviteId } = parsedBody.data;
+
+ logger.debug("signup", { email, password, inviteToken, inviteId });
const passwordHash = await hashPassword(password);
const userId = generateId(15);
+ if (config.flags?.disable_signup_without_invite) {
+ if (!inviteToken || !inviteId) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Signups are disabled without an invite code"
+ )
+ );
+ }
+
+ const { error, existingInvite } = await checkValidInvite({
+ token: inviteToken,
+ inviteId
+ });
+
+ if (error) {
+ return next(createHttpError(HttpCode.BAD_REQUEST, error));
+ }
+
+ if (!existingInvite) {
+ return next(
+ createHttpError(HttpCode.BAD_REQUEST, "Invite does not exist")
+ );
+ }
+ }
+
try {
const existing = await db
.select()
@@ -89,12 +120,15 @@ export async function signup(
if (diff < 2) {
// If the user was created less than 2 hours ago, we don't want to create a new user
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "A verification email was already sent to this email address. Please check your email for the verification code."
- )
- );
+ return response(res, {
+ data: {
+ emailVerificationRequired: true
+ },
+ success: true,
+ error: false,
+ message: `A user with that email address already exists. We sent an email to ${email} with a verification code.`,
+ status: HttpCode.OK
+ });
} else {
// If the user was created more than 2 hours ago, we want to delete the old user and create a new one
await db.delete(users).where(eq(users.userId, user.userId));
@@ -105,7 +139,7 @@ export async function signup(
userId: userId,
email: email,
passwordHash,
- dateCreated: moment().toISOString(),
+ dateCreated: moment().toISOString()
});
// give the user their default permissions:
@@ -125,12 +159,12 @@ export async function signup(
return response(res, {
data: {
- emailVerificationRequired: true,
+ emailVerificationRequired: true
},
success: true,
error: false,
message: `User created successfully. We sent an email to ${email} with a verification code.`,
- status: HttpCode.OK,
+ status: HttpCode.OK
});
}
@@ -139,7 +173,7 @@ export async function signup(
success: true,
error: false,
message: "User created successfully",
- status: HttpCode.OK,
+ status: HttpCode.OK
});
} catch (e) {
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts
index 8562b1d7..9d717979 100644
--- a/server/routers/org/createOrg.ts
+++ b/server/routers/org/createOrg.ts
@@ -28,6 +28,18 @@ export async function createOrg(
next: NextFunction
): Promise {
try {
+ // should this be in a middleware?
+ if (config.flags?.disable_user_create_org) {
+ if (!req.user?.serverAdmin) {
+ return next(
+ createHttpError(
+ HttpCode.FORBIDDEN,
+ "Only server admins can create organizations"
+ )
+ );
+ }
+ }
+
const parsedBody = createOrgSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts
index c097e5ff..2c08a537 100644
--- a/server/routers/user/acceptInvite.ts
+++ b/server/routers/user/acceptInvite.ts
@@ -11,6 +11,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "@server/auth/password";
+import { checkValidInvite } from "@server/auth/checkValidInvite";
const acceptInviteBodySchema = z
.object({
@@ -42,44 +43,25 @@ export async function acceptInvite(
const { token, inviteId } = parsedBody.data;
- const existingInvite = await db
- .select()
- .from(userInvites)
- .where(eq(userInvites.inviteId, inviteId))
- .limit(1);
-
- if (!existingInvite.length) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "Invite ID or token is invalid"
- )
- );
- }
-
- if (!isWithinExpirationDate(new Date(existingInvite[0].expiresAt))) {
- return next(
- createHttpError(HttpCode.BAD_REQUEST, "Invite has expired")
- );
- }
-
- const validToken = await verifyPassword(
+ const { error, existingInvite } = await checkValidInvite({
token,
- existingInvite[0].tokenHash
- );
- if (!validToken) {
+ inviteId
+ });
+
+ if (error) {
+ return next(createHttpError(HttpCode.BAD_REQUEST, error));
+ }
+
+ if (!existingInvite) {
return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "Invite ID or token is invalid"
- )
+ createHttpError(HttpCode.BAD_REQUEST, "Invite does not exist")
);
}
const existingUser = await db
.select()
.from(users)
- .where(eq(users.email, existingInvite[0].email))
+ .where(eq(users.email, existingInvite.email))
.limit(1);
if (!existingUser.length) {
return next(
@@ -90,7 +72,7 @@ export async function acceptInvite(
);
}
- if (req.user && req.user.email !== existingInvite[0].email) {
+ if (req.user && req.user.email !== existingInvite.email) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
@@ -104,7 +86,7 @@ export async function acceptInvite(
const existingRole = await db
.select()
.from(roles)
- .where(eq(roles.roleId, existingInvite[0].roleId))
+ .where(eq(roles.roleId, existingInvite.roleId))
.limit(1);
if (existingRole.length) {
roleId = existingRole[0].roleId;
@@ -122,8 +104,8 @@ export async function acceptInvite(
// add the user to the org
await trx.insert(userOrgs).values({
userId: existingUser[0].userId,
- orgId: existingInvite[0].orgId,
- roleId: existingInvite[0].roleId
+ orgId: existingInvite.orgId,
+ roleId: existingInvite.roleId
});
// delete the invite
@@ -131,9 +113,9 @@ export async function acceptInvite(
.delete(userInvites)
.where(eq(userInvites.inviteId, inviteId));
});
-
+
return response(res, {
- data: { accepted: true, orgId: existingInvite[0].orgId },
+ data: { accepted: true, orgId: existingInvite.orgId },
success: true,
error: false,
message: "Invite accepted",
diff --git a/server/routers/user/getUser.ts b/server/routers/user/getUser.ts
index 3a710458..b692ee6e 100644
--- a/server/routers/user/getUser.ts
+++ b/server/routers/user/getUser.ts
@@ -15,6 +15,7 @@ async function queryUser(userId: string) {
email: users.email,
twoFactorEnabled: users.twoFactorEnabled,
emailVerified: users.emailVerified,
+ serverAdmin: users.serverAdmin
})
.from(users)
.where(eq(users.userId, userId))
@@ -56,7 +57,7 @@ export async function getUser(
success: true,
error: false,
message: "User retrieved successfully",
- status: HttpCode.OK,
+ status: HttpCode.OK
});
} catch (error) {
logger.error(error);
diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts
index f301fdae..790166c7 100644
--- a/server/setup/copyInConfig.ts
+++ b/server/setup/copyInConfig.ts
@@ -7,9 +7,9 @@ import logger from "@server/logger";
export async function copyInConfig() {
// create a url from config.app.base_url and get the hostname
const domain = new URL(config.app.base_url).hostname;
-
+
// update the domain on all of the orgs where the domain is not equal to the new domain
// TODO: eventually each org could have a unique domain that we do not want to overwrite, so this will be unnecessary
await db.update(orgs).set({ domain }).where(ne(orgs.domain, domain));
- logger.debug("Updated orgs with new domain");
-}
\ No newline at end of file
+ logger.info(`Updated orgs with new domain (${domain})`);
+}
diff --git a/server/setup/ensureActions.ts b/server/setup/ensureActions.ts
index aa7b40f3..ec7b3a1e 100644
--- a/server/setup/ensureActions.ts
+++ b/server/setup/ensureActions.ts
@@ -36,7 +36,7 @@ export async function ensureActions() {
defaultRoles.map((role) => ({
roleId: role.roleId!,
actionId,
- orgId: role.orgId!,
+ orgId: role.orgId!
}))
)
.execute();
@@ -68,7 +68,7 @@ export async function createAdminRole(orgId: string) {
orgId,
isAdmin: true,
name: "Admin",
- description: "Admin role with the most permissions",
+ description: "Admin role with the most permissions"
})
.returning({ roleId: roles.roleId })
.execute();
@@ -92,7 +92,7 @@ export async function createAdminRole(orgId: string) {
actionIds.map((action) => ({
roleId,
actionId: action.actionId,
- orgId,
+ orgId
}))
)
.execute();
diff --git a/server/setup/index.ts b/server/setup/index.ts
index 8cd9521b..92d9ea05 100644
--- a/server/setup/index.ts
+++ b/server/setup/index.ts
@@ -2,10 +2,17 @@ import { ensureActions } from "./ensureActions";
import { copyInConfig } from "./copyInConfig";
import logger from "@server/logger";
import { runMigrations } from "./migrations";
+import { setupServerAdmin } from "./setupServerAdmin";
export async function runSetupFunctions() {
- logger.info(`Setup for version ${process.env.APP_VERSION}`);
- await runMigrations(); // run the migrations
- await ensureActions(); // make sure all of the actions are in the db and the roles
- await copyInConfig(); // copy in the config to the db as needed
-}
\ No newline at end of file
+ try {
+ logger.info(`Setup for version ${process.env.APP_VERSION}`);
+ await runMigrations(); // run the migrations
+ await copyInConfig(); // copy in the config to the db as needed
+ await setupServerAdmin();
+ await ensureActions(); // make sure all of the actions are in the db and the roles
+ } catch (error) {
+ logger.error("Error running setup functions", error);
+ process.exit(1);
+ }
+}
diff --git a/server/setup/setupServerAdmin.ts b/server/setup/setupServerAdmin.ts
new file mode 100644
index 00000000..a0b6696e
--- /dev/null
+++ b/server/setup/setupServerAdmin.ts
@@ -0,0 +1,86 @@
+import { generateId, invalidateAllSessions } from "@server/auth";
+import { hashPassword, verifyPassword } from "@server/auth/password";
+import { passwordSchema } from "@server/auth/passwordSchema";
+import config from "@server/config";
+import db from "@server/db";
+import { users } from "@server/db/schema";
+import logger from "@server/logger";
+import { eq } from "drizzle-orm";
+import moment from "moment";
+import { fromError } from "zod-validation-error";
+
+export async function setupServerAdmin() {
+ const {
+ server_admin: { email, password }
+ } = config.users;
+
+ const parsed = passwordSchema.safeParse(password);
+
+ if (!parsed.success) {
+ throw Error(
+ `Invalid server admin password: ${fromError(parsed.error).toString()}`
+ );
+ }
+
+ const passwordHash = await hashPassword(password);
+
+ await db.transaction(async (trx) => {
+ try {
+ const [existing] = await trx
+ .select()
+ .from(users)
+ .where(eq(users.email, email));
+
+ if (existing) {
+ const passwordChanged = !(await verifyPassword(
+ password,
+ existing.passwordHash
+ ));
+
+ if (passwordChanged) {
+ await trx
+ .update(users)
+ .set({ passwordHash })
+ .where(eq(users.email, email));
+
+ // this isn't using the transaction, but it's probably fine
+ await invalidateAllSessions(existing.userId);
+
+ logger.info(`Server admin (${email}) password updated`);
+ }
+
+ if (existing.serverAdmin) {
+ return;
+ }
+
+ await trx.update(users).set({ serverAdmin: false });
+
+ await trx
+ .update(users)
+ .set({
+ serverAdmin: true
+ })
+ .where(eq(users.email, email));
+
+ logger.info(`Server admin (${email}) updated`);
+ return;
+ }
+
+ const userId = generateId(15);
+
+ await db.insert(users).values({
+ userId: userId,
+ email: email,
+ passwordHash,
+ dateCreated: moment().toISOString(),
+ serverAdmin: true,
+ emailVerified: true
+ });
+
+ logger.info(`Server admin (${email}) created`);
+ } catch (e) {
+ logger.error(e);
+ trx.rollback();
+ }
+ });
+}
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index 4508d753..3b3b1289 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -3,6 +3,7 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { cache } from "react";
import DashboardLoginForm from "./DashboardLoginForm";
+import { Mail } from "lucide-react";
export const dynamic = "force-dynamic";
@@ -13,27 +14,48 @@ export default async function Page(props: {
const getUser = cache(verifySession);
const user = await getUser();
+ const isInvite = searchParams?.redirect?.includes("/invite");
+
+ const signUpDisabled = process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true";
+
if (user) {
redirect("/");
}
return (
<>
+ {isInvite && (
+
+
+
+
+ Looks like you've been invited!
+
+
+ To accept the invite, you must login or create an
+ account.
+
+
+
+ )}
+
-
- Don't have an account?{" "}
-
- Sign up
-
-
+ {(!signUpDisabled || isInvite) && (
+
+ Don't have an account?{" "}
+
+ Sign up
+
+
+ )}
>
);
}
diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx
index b574ba4e..d2194770 100644
--- a/src/app/auth/signup/SignupForm.tsx
+++ b/src/app/auth/signup/SignupForm.tsx
@@ -32,6 +32,8 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
type SignupFormProps = {
redirect?: string;
+ inviteId?: string;
+ inviteToken?: string;
};
const formSchema = z
@@ -45,7 +47,7 @@ const formSchema = z
message: "Passwords do not match",
});
-export default function SignupForm({ redirect }: SignupFormProps) {
+export default function SignupForm({ redirect, inviteId, inviteToken }: SignupFormProps) {
const router = useRouter();
const api = createApiClient(useEnvContext());
@@ -70,6 +72,8 @@ export default function SignupForm({ redirect }: SignupFormProps) {
.put>("/auth/signup", {
email,
password,
+ inviteId,
+ inviteToken
})
.catch((e) => {
console.error(e);
diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx
index 785a168c..e3e8fe98 100644
--- a/src/app/auth/signup/page.tsx
+++ b/src/app/auth/signup/page.tsx
@@ -1,25 +1,65 @@
import SignupForm from "@app/app/auth/signup/SignupForm";
import { verifySession } from "@app/lib/auth/verifySession";
+import { Mail } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { cache } from "react";
-export const dynamic = 'force-dynamic';
+export const dynamic = "force-dynamic";
export default async function Page(props: {
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+ searchParams: Promise<{ redirect: string | undefined }>;
}) {
const searchParams = await props.searchParams;
const getUser = cache(verifySession);
const user = await getUser();
+ const isInvite = searchParams?.redirect?.includes("/invite");
+
+ if (process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true" && !isInvite) {
+ redirect("/");
+ }
+
if (user) {
redirect("/");
}
+ let inviteId;
+ let inviteToken;
+ if (searchParams.redirect && isInvite) {
+ const parts = searchParams.redirect.split("token=");
+ if (parts.length) {
+ const token = parts[1];
+ const tokenParts = token.split("-");
+ if (tokenParts.length === 2) {
+ inviteId = tokenParts[0];
+ inviteToken = tokenParts[1];
+ }
+ }
+ }
+
return (
<>
-
+ {isInvite && (
+
+
+
+
+ Looks like you've been invited!
+
+
+ To accept the invite, you must login or create an
+ account.
+
+
+
+ )}
+
+
Already have an account?{" "}
diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx
index a02e5862..7af05536 100644
--- a/src/app/invite/page.tsx
+++ b/src/app/invite/page.tsx
@@ -21,7 +21,7 @@ export default async function InvitePage(props: {
const user = await verifySession();
if (!user) {
- redirect(`/?redirect=/invite?token=${params.token}`);
+ redirect(`/auth/signup?redirect=/invite?token=${params.token}`);
}
const parts = tokenParam.split("-");
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2271bcff..d9792294 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -37,7 +37,10 @@ export default async function RootLayout({
SERVER_EXTERNAL_PORT: process.env
.SERVER_EXTERNAL_PORT as string,
ENVIRONMENT: process.env.ENVIRONMENT as string,
- EMAIL_ENABLED: process.env.EMAIL_ENABLED as string
+ EMAIL_ENABLED: process.env.EMAIL_ENABLED as string,
+ // optional
+ DISABLE_USER_CREATE_ORG: process.env.DISABLE_USER_CREATE_ORG,
+ DISABLE_SIGNUP_WITHOUT_INVITE: process.env.DISABLE_SIGNUP_WITHOUT_INVITE,
}}
>
{children}
diff --git a/src/app/profile/general/layout_.tsx b/src/app/profile/general/layout_.tsx
deleted file mode 100644
index 947b3338..00000000
--- a/src/app/profile/general/layout_.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
-import { SidebarSettings } from "@app/components/SidebarSettings";
-import { verifySession } from "@app/lib/auth/verifySession";
-import UserProvider from "@app/providers/UserProvider";
-import { redirect } from "next/navigation";
-import { cache } from "react";
-
-type ProfileGeneralProps = {
- children: React.ReactNode;
-};
-
-export default async function GeneralSettingsPage({
- children
-}: ProfileGeneralProps) {
- const getUser = cache(verifySession);
- const user = await getUser();
-
- if (!user) {
- redirect(`/?redirect=/profile/general`);
- }
-
- const sidebarNavItems = [
- {
- title: "Authentication",
- href: `/{orgId}/settings/general`
- }
- ];
-
- return (
- <>
-
- {children}
-
- >
- );
-}
diff --git a/src/app/profile/general/page.tsx b/src/app/profile/general/page.tsx
deleted file mode 100644
index 9c85c0bb..00000000
--- a/src/app/profile/general/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import Enable2FaForm from "../../../components/Enable2FaForm";
-
-export default function ProfileGeneralPage() {
- const [open, setOpen] = useState(true);
-
- return (
- <>
- {/* */}
- >
- );
-}
diff --git a/src/app/profile/general/page_.tsx b/src/app/profile/general/page_.tsx
deleted file mode 100644
index 7be110b2..00000000
--- a/src/app/profile/general/page_.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import Enable2FaForm from "@app/components/Enable2FaForm";
-
-export default function ProfileGeneralPage() {
- const [open, setOpen] = useState(true);
-
- return (
- <>
-
- >
- );
-}
diff --git a/src/app/profile/layout_.tsx b/src/app/profile/layout_.tsx
deleted file mode 100644
index f2d73776..00000000
--- a/src/app/profile/layout_.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Metadata } from "next";
-import { verifySession } from "@app/lib/auth/verifySession";
-import { redirect } from "next/navigation";
-import { cache } from "react";
-import Header from "@app/components/Header";
-import { internal } from "@app/api";
-import { AxiosResponse } from "axios";
-import { ListOrgsResponse } from "@server/routers/org";
-import { authCookieHeader } from "@app/api/cookies";
-import { TopbarNav } from "@app/components/TopbarNav";
-import { Settings } from "lucide-react";
-
-export const dynamic = "force-dynamic";
-
-export const metadata: Metadata = {
- title: `User Settings - Pangolin`,
- description: ""
-};
-
-const topNavItems = [
- {
- title: "User Settings",
- href: "/profile/general",
- icon:
- }
-];
-
-interface SettingsLayoutProps {
- children: React.ReactNode;
- params: Promise<{}>;
-}
-
-export default async function SettingsLayout(props: SettingsLayoutProps) {
- const { children } = props;
-
- const getUser = cache(verifySession);
- const user = await getUser();
-
- if (!user) {
- redirect(`/`);
- }
-
- const cookie = await authCookieHeader();
-
- let orgs: ListOrgsResponse["orgs"] = [];
- try {
- const getOrgs = cache(() =>
- internal.get>(`/orgs`, cookie)
- );
- const res = await getOrgs();
- if (res && res.data.data.orgs) {
- orgs = res.data.data.orgs;
- }
- } catch (e) {
- console.error("Error fetching orgs", e);
- }
-
- return (
- <>
-
-
-
- {children}
-
- >
- );
-}
diff --git a/src/app/profile/page_.tsx b/src/app/profile/page_.tsx
deleted file mode 100644
index f1dafa49..00000000
--- a/src/app/profile/page_.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from "next/navigation";
-
-export default async function ProfilePage() {
- redirect("/profile/general");
-}
diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx
index 139c1701..0e32ee0a 100644
--- a/src/app/setup/layout.tsx
+++ b/src/app/setup/layout.tsx
@@ -22,5 +22,13 @@ export default async function SetupLayout({
redirect("/?redirect=/setup");
}
- return {children}
;
+ if (
+ !(process.env.DISABLE_USER_CREATE_ORG === "false" || user.serverAdmin)
+ ) {
+ redirect("/");
+ }
+
+ return (
+ {children}
+ );
}
diff --git a/src/components/Disable2FaForm.tsx b/src/components/Disable2FaForm.tsx
index e3e9f168..68e51a67 100644
--- a/src/components/Disable2FaForm.tsx
+++ b/src/components/Disable2FaForm.tsx
@@ -96,12 +96,18 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
setLoading(false);
};
+ function reset() {
+ disableForm.reset();
+ setStep("password");
+ setLoading(false);
+ }
+
return (
{
setOpen(val);
- setLoading(false);
+ reset();
}}
>
diff --git a/src/components/Enable2FaForm.tsx b/src/components/Enable2FaForm.tsx
index 0bdaf1fd..4e66f23a 100644
--- a/src/components/Enable2FaForm.tsx
+++ b/src/components/Enable2FaForm.tsx
@@ -154,12 +154,25 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
}
};
+ function reset() {
+ setLoading(false);
+ setStep(1);
+ setSecretKey("");
+ setSecretUri("");
+ setVerificationCode("");
+ setError("");
+ setSuccess(false);
+ setBackupCodes([]);
+ enableForm.reset();
+ confirmForm.reset();
+ }
+
return (
{
setOpen(val);
- setLoading(false);
+ reset();
}}
>
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 4c8adeaa..88faebde 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -67,7 +67,9 @@ export function Header({ orgId, orgs }: HeaderProps) {
const router = useRouter();
- const api = createApiClient(useEnvContext());
+ const { env } = useEnvContext();
+
+ const api = createApiClient({ env });
function getInitials() {
return user.email.substring(0, 2).toUpperCase();
@@ -126,6 +128,11 @@ export function Header({ orgId, orgs }: HeaderProps) {
{user.email}
+ {user.serverAdmin && (
+
+ Server Admin
+
+ )}
{!user.twoFactorEnabled && (
@@ -237,19 +244,28 @@ export function Header({ orgId, orgs }: HeaderProps) {
No organizations found.
-
-
- {
- router.push("/setup");
- }}
- >
-
- New Organization
-
-
-
-
+ {(env.DISABLE_USER_CREATE_ORG === "false" ||
+ user.serverAdmin) && (
+ <>
+
+
+ {
+ router.push(
+ "/setup"
+ );
+ }}
+ >
+
+ New Organization
+
+
+
+
+ >
+ )}
{orgs.map((org) => (
diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx
index 0d9402f3..90567bd4 100644
--- a/src/components/LoginForm.tsx
+++ b/src/components/LoginForm.tsx
@@ -57,7 +57,9 @@ const mfaSchema = z.object({
export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
const router = useRouter();
- const api = createApiClient(useEnvContext());
+ const { env } = useEnvContext();
+
+ const api = createApiClient({ env });
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts
index 33e7d79d..d0a26f94 100644
--- a/src/lib/types/env.ts
+++ b/src/lib/types/env.ts
@@ -3,4 +3,6 @@ export type env = {
NEXT_PORT: string;
ENVIRONMENT: string;
EMAIL_ENABLED: string;
+ DISABLE_SIGNUP_WITHOUT_INVITE?: string;
+ DISABLE_USER_CREATE_ORG?: string;
};