Login, logout and middleware updates

This commit is contained in:
Darren Clarke 2024-12-13 16:37:20 +01:00
parent f552f8024f
commit 9fb3665ced
18 changed files with 96 additions and 50 deletions

View file

@ -13,7 +13,7 @@ export const Setup: FC = () => {
} = useLeafcutterContext();
const router = useRouter();
useLayoutEffect(() => {
setTimeout(() => router.push("/"), 4000);
setTimeout(() => router.push("/"), 2000);
}, [router]);
return (

View file

@ -29,6 +29,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
typeof window !== "undefined" && window.location.origin
? window.location.origin
: "";
const callbackUrl = `${origin}/setup`;
const [provider, setProvider] = useState(undefined);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@ -157,7 +158,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
sx={buttonStyles}
onClick={() =>
signIn("google", {
callbackUrl: `${origin}`,
callbackUrl,
})
}
>
@ -173,7 +174,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
sx={buttonStyles}
onClick={() =>
signIn("apple", {
callbackUrl: `${window.location.origin}`,
callbackUrl,
})
}
>
@ -214,7 +215,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
signIn("credentials", {
email,
password,
callbackUrl: `${origin}/setup`,
callbackUrl,
})
}
>

View file

@ -1,8 +1,9 @@
"use client";
import { ReactNode } from "react";
import dynamic from "next/dynamic";
type ClientOnlyProps = { children: JSX.Element };
type ClientOnlyProps = { children: ReactNode };
const ClientOnly = (props: ClientOnlyProps) => {
const { children } = props;

View file

@ -69,7 +69,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
}, [session]);
if (!session || !authenticated) {
console.log("Not authenticated");
return (
<Box sx={{ width: "100%" }}>
<Grid
@ -89,7 +88,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
}
if (session && authenticated) {
console.log("Session and authenticated");
return (
<Iframe
id={id}
@ -102,10 +100,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
const linkElement = document.querySelector(
`#${id}`,
) as HTMLIFrameElement;
console.log({ path });
console.log({ id });
console.log({ linkElement });
if (
linkElement.contentDocument &&
linkElement.contentDocument?.querySelector &&

View file

@ -1,9 +1,20 @@
"use client";
import { useEffect } from "react";
import { signOut } from "next-auth/react";
export default function Page() {
useEffect(() => {
const multistepSignOut = async () => {
const response = await fetch("/api/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
});
signOut({ callbackUrl: "/login" });
};
multistepSignOut();
}, []);
return <div />;
}

View file

@ -1,7 +1,6 @@
"use client";
import { FC, useState, useEffect } from "react";
import { useFormState } from "react-dom";
import { FC, useState, useEffect, useActionState } from "react";
import { useRouter } from "next/navigation";
import { Grid } from "@mui/material";
import {
@ -44,7 +43,7 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
},
},
};
const [formState, formAction] = useFormState(
const [formState, formAction] = useActionState(
createTicketAction,
initialState,
);

View file

@ -65,7 +65,6 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
columns={gridColumns}
onRowClick={onRowClick}
getRowID={(row: any) => {
console.log({ row });
return row.internalId;
}}
buttons={

View file

@ -13,6 +13,7 @@ type ZammadOverviewProps = {
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
const [tickets, setTickets] = useState([]);
if (typeof window !== "undefined") {
useEffect(() => {
const hash = window?.location?.hash;
@ -23,6 +24,7 @@ export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
}
}
}, [window?.location?.hash]);
}
useEffect(() => {
const fetchTickets = async () => {

View file

@ -8,6 +8,13 @@ import { ZammadWrapper } from "app/(main)/_components/ZammadWrapper";
export const Setup: FC = () => {
const router = useRouter();
useLayoutEffect(() => {
const fingerprint = localStorage.getItem("fingerprint");
if (!fingerprint || fingerprint === "") {
const newFingerprint = `${Math.floor(
Math.random() * 100000000,
)}`.padStart(8, "0");
localStorage.setItem("fingerprint", newFingerprint);
}
setTimeout(() => router.push("/"), 4000);
}, [router]);

View file

@ -37,11 +37,6 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const [agents, setAgents] = useState<any>();
const [pendingVisible, setPendingVisible] = useState(false);
const filteredStates =
ticketStates?.filter(
(state: any) => !["new", "merged", "removed"].includes(state.label),
) ?? [];
useEffect(() => {
const fetchAgents = async () => {
const groupID = formState?.values?.group?.split("/")?.pop();
@ -96,7 +91,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
[name]: value,
},
});
const stateName = filteredStates?.find(
const stateName = ticketStates?.find(
(state: any) => state.id === formState.values.state,
)?.name;
setPendingVisible(stateName?.includes("pending") ?? false);
@ -141,7 +136,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
label="State"
formState={formState}
updateFormState={updateFormState}
getOptions={() => filteredStates}
getOptions={() => ticketStates}
/>
</Grid>
<Grid

View file

@ -47,10 +47,10 @@ export const getOverviewTicketsAction = async (name: string) => {
try {
if (name === "Recent") {
const recent = await executeREST({ path: "/api/v1/recent_view" });
for (const rec of recent) {
const uniqueIDs = new Set(recent.map((rec: any) => rec.o_id));
for (const id of uniqueIDs) {
const tkt = await executeREST({
path: `/api/v1/tickets/${rec.o_id}`,
path: `/api/v1/tickets/${id}`,
});
tickets.push({
...tkt,

View file

@ -1,6 +1,5 @@
"use server";
import { revalidatePath } from "next/cache";
import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
import { createTicketMutation } from "app/_graphql/createTicketMutation";
@ -157,13 +156,14 @@ export const getTicketStatesAction = async () => {
const states = await executeREST({
path: "/api/v1/ticket_states",
});
console.log({ states });
const formattedStates =
states?.map((state: any) => ({
value: `gid://zammad/Ticket::State/${state.id}`,
label: state.name,
disabled: ["new", "merged", "removed"].includes(state.name),
})) ?? [];
console.log({ formattedStates });
return formattedStates;
} catch (e) {
console.error(e.message);

View file

@ -14,7 +14,7 @@ export const ZammadLoginProvider: FC<PropsWithChildren> = ({ children }) => {
const response = await fetch("/api/v1/users/me", {
method: "GET",
headers: {
"X-Browser-Fingerprint": `${session.expires}`,
"X-Browser-Fingerprint": localStorage.getItem("fingerprint") || "",
},
});

View file

@ -110,7 +110,7 @@ export const authOptions: NextAuthOptions = {
},
providers,
session: {
maxAge: 7 * 24 * 60 * 60,
maxAge: 3 * 24 * 60 * 60,
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {

View file

@ -1,19 +1,12 @@
import { getServerSession } from "app/_lib/authentication";
import { cookies, headers } from "next/headers";
import crypto from "crypto";
import { cookies } from "next/headers";
const getHeaders = async () => {
const userAgent = (await headers()).get("user-agent");
const allCookies = (await cookies()).getAll();
const hashedUserAgent = crypto
.createHash("sha256")
.update(userAgent)
.digest("hex");
const session = await getServerSession();
const finalHeaders = {
"Content-Type": "application/json",
Accept: "application/json",
"X-Browser-Fingerprint": hashedUserAgent,
// @ts-ignore
"X-CSRF-Token": session.user.zammadCsrfToken,
Cookie: allCookies

View file

@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const allCookies = request.cookies.getAll();
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
const signOutURL = `${zammadURL}/api/v1/signout`;
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
Cookie: allCookies
.map((cookie) => `${cookie.name}=${cookie.value}`)
.join("; "),
};
await fetch(signOutURL, { headers });
const cookiePrefixesToRemove = ["_zammad"];
const response = NextResponse.json({ message: "ok" });
for (const cookie of allCookies) {
if (
cookiePrefixesToRemove.some((prefix) => cookie.name.startsWith(prefix))
) {
response.cookies.set(cookie.name, "", { path: "/", maxAge: 0 });
}
}
return response;
}

View file

@ -14,6 +14,10 @@ const rewriteURL = (
const destinationURL = `${destinationBaseURL}/${path}`;
console.log(`Rewriting ${request.url} to ${destinationURL}`);
const requestHeaders = new Headers(request.headers);
for (const [key, value] of requestHeaders.entries()) {
console.log(`${key}: ${value}`);
}
requestHeaders.delete("x-forwarded-user");
requestHeaders.delete("x-forwarded-roles");
requestHeaders.delete("connection");
@ -61,6 +65,12 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers);
} else if (zammadPaths.some((p) => request.nextUrl.pathname.startsWith(p))) {
return rewriteURL(request, linkBaseURL, zammadURL, headers);
} else if (request.nextUrl.pathname.startsWith("/api/v1")) {
if (email && email !== "unknown") {
return NextResponse.next();
} else {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
} else {
const isDev = process.env.NODE_ENV === "development";
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
@ -75,7 +85,7 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
frame-ancestors 'self';
upgrade-insecure-requests;
`;
const contentSecurityPolicyHeaderValue = cspHeader
@ -132,5 +142,5 @@ export default withAuth(checkRewrites, {
});
export const config = {
matcher: ["/((?!ws|wss|api|_next/static|_next/image|favicon.ico).*)"],
matcher: ["/((?!ws|wss|_next/static|_next/image|favicon.ico).*)"],
};

View file

@ -51,7 +51,12 @@ export const Select: FC<SelectProps> = ({
}}
>
{options.map((option: SelectOption) => (
<MenuItem key={option.value} value={option.value}>
<MenuItem
key={option.value}
value={option.value}
// @ts-ignore
disabled={option.disabled ?? false}
>
{option.label}
</MenuItem>
))}