mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-12 16:06:38 +00:00
setup server admin
This commit is contained in:
@@ -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 && (
|
||||
<div className="border rounded-md p-3 mb-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<Mail className="w-12 h-12 mb-4 text-primary" />
|
||||
<h2 className="text-2xl font-bold mb-2 text-center">
|
||||
Looks like you've been invited!
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
To accept the invite, you must login or create an
|
||||
account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DashboardLoginForm redirect={searchParams.redirect as string} />
|
||||
|
||||
<p className="text-center text-muted-foreground mt-4">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href={
|
||||
!searchParams.redirect
|
||||
? `/auth/signup`
|
||||
: `/auth/signup?redirect=${searchParams.redirect}`
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
{(!signUpDisabled || isInvite) && (
|
||||
<p className="text-center text-muted-foreground mt-4">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href={
|
||||
!searchParams.redirect
|
||||
? `/auth/signup`
|
||||
: `/auth/signup?redirect=${searchParams.redirect}`
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<AxiosResponse<SignUpResponse>>("/auth/signup", {
|
||||
email,
|
||||
password,
|
||||
inviteId,
|
||||
inviteToken
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SignupForm redirect={searchParams.redirect as string} />
|
||||
{isInvite && (
|
||||
<div className="border rounded-md p-3 mb-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<Mail className="w-12 h-12 mb-4 text-primary" />
|
||||
<h2 className="text-2xl font-bold mb-2 text-center">
|
||||
Looks like you've been invited!
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
To accept the invite, you must login or create an
|
||||
account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SignupForm
|
||||
redirect={searchParams.redirect as string}
|
||||
inviteToken={inviteToken}
|
||||
inviteId={inviteId}
|
||||
/>
|
||||
|
||||
<p className="text-center text-muted-foreground mt-4">
|
||||
Already have an account?{" "}
|
||||
|
||||
@@ -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("-");
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<UserProvider user={user}>
|
||||
{children}
|
||||
</UserProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
{/* <Enable2FaForm open={open} setOpen={setOpen} /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Enable2FaForm open={open} setOpen={setOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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: <Settings className="h-4 w-4" />
|
||||
}
|
||||
];
|
||||
|
||||
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<AxiosResponse<ListOrgsResponse>>(`/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 (
|
||||
<>
|
||||
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 select-none sm:px-0 px-3 fixed top-0 z-10">
|
||||
<div className="container mx-auto flex flex-col content-between">
|
||||
<div className="my-4">
|
||||
<Header email={user.email} orgs={orgs} />
|
||||
</div>
|
||||
<TopbarNav items={topNavItems} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto sm:px-0 px-3 pt-[165px]">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
redirect("/profile/general");
|
||||
}
|
||||
@@ -22,5 +22,13 @@ export default async function SetupLayout({
|
||||
redirect("/?redirect=/setup");
|
||||
}
|
||||
|
||||
return <div className="w-full max-w-2xl mx-auto p-3 md:mt-32">{children}</div>;
|
||||
if (
|
||||
!(process.env.DISABLE_USER_CREATE_ORG === "false" || user.serverAdmin)
|
||||
) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto p-3 md:mt-32">{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,12 +96,18 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
function reset() {
|
||||
disableForm.reset();
|
||||
setStep("password");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
setLoading(false);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
|
||||
@@ -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 (
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
setLoading(false);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
|
||||
@@ -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}
|
||||
</p>
|
||||
</div>
|
||||
{user.serverAdmin && (
|
||||
<p className="text-xs leading-none text-muted-foreground mt-2">
|
||||
Server Admin
|
||||
</p>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{!user.twoFactorEnabled && (
|
||||
@@ -237,19 +244,28 @@ export function Header({ orgId, orgs }: HeaderProps) {
|
||||
<CommandEmpty>
|
||||
No organizations found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading="Create">
|
||||
<CommandList>
|
||||
<CommandItem
|
||||
onSelect={(currentValue) => {
|
||||
router.push("/setup");
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Organization
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
{(env.DISABLE_USER_CREATE_ORG === "false" ||
|
||||
user.serverAdmin) && (
|
||||
<>
|
||||
<CommandGroup heading="Create">
|
||||
<CommandList>
|
||||
<CommandItem
|
||||
onSelect={(
|
||||
currentValue
|
||||
) => {
|
||||
router.push(
|
||||
"/setup"
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Organization
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)}
|
||||
<CommandGroup heading="Organizations">
|
||||
<CommandList>
|
||||
{orgs.map((org) => (
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user