successful log in loop poc

This commit is contained in:
miloschwartz
2025-04-13 17:57:27 -04:00
parent 7556a59e11
commit 53be2739bb
37 changed files with 789 additions and 474 deletions

View File

@@ -24,7 +24,13 @@ import { useUserContext } from "@app/hooks/useUserContext";
export type UserRow = {
id: string;
email: string;
email: string | null;
displayUsername: string | null;
username: string;
name: string | null;
idpId: number | null;
idpName: string;
type: string;
status: string;
role: string;
isOwner: boolean;
@@ -82,7 +88,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
Manage User
</DropdownMenuItem>
</Link>
{userRow.email !== user?.email && (
{userRow.username !==
user?.username && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
@@ -108,7 +115,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
}
},
{
accessorKey: "email",
accessorKey: "displayUsername",
header: ({ column }) => {
return (
<Button
@@ -117,14 +124,14 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Email
Username
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: "status",
accessorKey: "idpName",
header: ({ column }) => {
return (
<Button
@@ -133,7 +140,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Status
Identity Provider
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -185,7 +192,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
<Button
variant={"outlinePrimary"}
className="ml-2"
>
Manage
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
@@ -239,7 +249,12 @@ export default function UsersTable({ users: u }: UsersTableProps) {
<div className="space-y-4">
<p>
Are you sure you want to remove{" "}
<b>{selectedUser?.email}</b> from the organization?
<b>
{selectedUser?.email ||
selectedUser?.name ||
selectedUser?.username}
</b>{" "}
from the organization?
</p>
<p>
@@ -250,14 +265,19 @@ export default function UsersTable({ users: u }: UsersTableProps) {
</p>
<p>
To confirm, please type the email address of the
user below.
To confirm, please type the name of the of the user
below.
</p>
</div>
}
buttonText="Confirm Remove User"
onConfirm={removeUser}
string={selectedUser?.email ?? ""}
string={
selectedUser?.email ||
selectedUser?.name ||
selectedUser?.username ||
""
}
title="Remove User from Organization"
/>

View File

@@ -70,7 +70,13 @@ export default async function UsersPage(props: UsersPageProps) {
const userRows: UserRow[] = users.map((user) => {
return {
id: user.id,
username: user.username,
displayUsername: user.email || user.name || user.username,
name: user.name,
email: user.email,
type: user.type,
idpId: user.idpId,
idpName: user.idpName || "Internal",
status: "Confirmed",
role: user.isOwner ? "Owner" : user.roleName || "Member",
isOwner: user.isOwner || false

View File

@@ -45,6 +45,7 @@ import { SwitchInput } from "@app/components/SwitchInput";
import { InfoPopup } from "@app/components/ui/info-popup";
import { Tag, TagInput } from "@app/components/tags/tag-input";
import { useRouter } from "next/navigation";
import { UserType } from "@server/types/UserTypes";
const UsersRolesFormSchema = z.object({
roles: z.array(
@@ -175,7 +176,7 @@ export default function ResourceAuthenticationPage() {
setAllUsers(
usersResponse.data.data.users.map((user) => ({
id: user.id.toString(),
text: user.email
text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
}))
);
@@ -183,7 +184,7 @@ export default function ResourceAuthenticationPage() {
"users",
resourceUsersResponse.data.data.users.map((i) => ({
id: i.userId.toString(),
text: i.email
text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}`
}))
);

View File

@@ -14,7 +14,12 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
export type GlobalUserRow = {
id: string;
email: string;
name: string | null;
username: string;
email: string | null;
type: string;
idpId: number | null;
idpName: string;
dateCreated: string;
};
@@ -67,6 +72,22 @@ export default function UsersTable({ users }: Props) {
);
}
},
{
accessorKey: "username",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Username
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: "email",
header: ({ column }) => {
@@ -83,6 +104,38 @@ export default function UsersTable({ users }: Props) {
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: "idpName",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Identity Provider
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
id: "actions",
cell: ({ row }) => {
@@ -120,8 +173,12 @@ export default function UsersTable({ users }: Props) {
<div className="space-y-4">
<p>
Are you sure you want to permanently delete{" "}
<b>{selected?.email || selected?.id}</b> from
the server?
<b>
{selected?.email ||
selected?.name ||
selected?.username}
</b>{" "}
from the server?
</p>
<p>
@@ -133,14 +190,16 @@ export default function UsersTable({ users }: Props) {
</p>
<p>
To confirm, please type the email of the user
To confirm, please type the name of the user
below.
</p>
</div>
}
buttonText="Confirm Delete User"
onConfirm={async () => deleteUser(selected!.id)}
string={selected.email}
string={
selected.email || selected.name || selected.username
}
title="Delete User from Server"
/>
)}

View File

@@ -27,6 +27,11 @@ export default async function UsersPage(props: PageProps) {
return {
id: row.id,
email: row.email,
name: row.name,
username: row.username,
type: row.type,
idpId: row.idpId,
idpName: row.idpName || "Internal",
dateCreated: row.dateCreated,
serverAdmin: row.serverAdmin
};

View File

@@ -4,56 +4,58 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { ValidateOidcUrlCallbackResponse } from "@server/routers/idp";
import { AxiosResponse } from "axios";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
type ValidateOidcTokenParams = {
orgId: string;
idpId: string;
code: string | undefined;
verifier: string | undefined;
storedState: string | undefined;
expectedState: string | undefined;
stateCookie: string | undefined;
};
export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
const { env } = useEnvContext();
const api = createApiClient({ env });
const router = useRouter();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!props.code || !props.verifier) {
setError("Missing code or verifier");
setLoading(false);
return;
}
if (!props.storedState) {
setError("Missing stored state");
setLoading(false);
return;
}
if (props.storedState !== props.expectedState) {
setError("Invalid state");
setLoading(false);
return;
}
async function validate() {
setLoading(true);
console.log("Validating OIDC token", {
code: props.code,
expectedState: props.expectedState,
stateCookie: props.stateCookie
});
try {
const res = await api.post<
AxiosResponse<ValidateOidcUrlCallbackResponse>
>(
`/auth/org/${props.orgId}/idp/${props.idpId}/oidc/validate-callback`,
{
code: props.code,
codeVerifier: props.verifier
}
);
>(`/auth/idp/${props.idpId}/oidc/validate-callback`, {
code: props.code,
state: props.expectedState,
storedState: props.stateCookie
});
console.log("Validate OIDC token response", res.data);
const redirectUrl = res.data.data.redirectUrl;
if (!redirectUrl) {
router.push("/");
}
if (redirectUrl.startsWith("http")) {
window.location.href = res.data.data.redirectUrl; // TODO: validate this to make sure it's safe
} else {
router.push(res.data.data.redirectUrl);
}
} catch (e) {
setError(formatAxiosError(e, "Error validating OIDC token"));
} finally {

View File

@@ -12,8 +12,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
const allCookies = await cookies();
const stateCookie = allCookies.get("oidc_state")?.value;
const verifier = allCookies.get("oidc_code_verifier")?.value;
const stateCookie = allCookies.get("p_oidc_state")?.value;
return (
<>
@@ -21,9 +20,8 @@ export default async function Page(props: {
orgId={params.orgId}
idpId={params.idpId}
code={searchParams.code}
storedState={stateCookie}
expectedState={searchParams.state}
verifier={verifier}
stateCookie={stateCookie}
/>
</>
);

View File

@@ -36,7 +36,7 @@ export default async function Page(props: {
return (
<>
<VerifyEmailForm
email={user.email}
email={user.email!}
redirect={redirectUrl}
/>
</>

View File

@@ -35,7 +35,13 @@ export const orgNavItems: SidebarNavItem[] = [
children: [
{
title: "Users",
href: "/{orgId}/settings/access/users"
href: "/{orgId}/settings/access/users",
children: [
{
title: "Invitations",
href: "/{orgId}/settings/access/invitations"
}
]
},
{
title: "Roles",