Login, logout and middleware updates
This commit is contained in:
parent
f552f8024f
commit
9fb3665ced
18 changed files with 96 additions and 50 deletions
|
|
@ -13,7 +13,7 @@ export const Setup: FC = () => {
|
||||||
} = useLeafcutterContext();
|
} = useLeafcutterContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setTimeout(() => router.push("/"), 4000);
|
setTimeout(() => router.push("/"), 2000);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
typeof window !== "undefined" && window.location.origin
|
typeof window !== "undefined" && window.location.origin
|
||||||
? window.location.origin
|
? window.location.origin
|
||||||
: "";
|
: "";
|
||||||
|
const callbackUrl = `${origin}/setup`;
|
||||||
const [provider, setProvider] = useState(undefined);
|
const [provider, setProvider] = useState(undefined);
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
@ -157,7 +158,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
sx={buttonStyles}
|
sx={buttonStyles}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
signIn("google", {
|
signIn("google", {
|
||||||
callbackUrl: `${origin}`,
|
callbackUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -173,7 +174,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
sx={buttonStyles}
|
sx={buttonStyles}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
signIn("apple", {
|
signIn("apple", {
|
||||||
callbackUrl: `${window.location.origin}`,
|
callbackUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -214,7 +215,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
signIn("credentials", {
|
signIn("credentials", {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
callbackUrl: `${origin}/setup`,
|
callbackUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
type ClientOnlyProps = { children: JSX.Element };
|
type ClientOnlyProps = { children: ReactNode };
|
||||||
const ClientOnly = (props: ClientOnlyProps) => {
|
const ClientOnly = (props: ClientOnlyProps) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
if (!session || !authenticated) {
|
if (!session || !authenticated) {
|
||||||
console.log("Not authenticated");
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: "100%" }}>
|
<Box sx={{ width: "100%" }}>
|
||||||
<Grid
|
<Grid
|
||||||
|
|
@ -89,7 +88,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && authenticated) {
|
if (session && authenticated) {
|
||||||
console.log("Session and authenticated");
|
|
||||||
return (
|
return (
|
||||||
<Iframe
|
<Iframe
|
||||||
id={id}
|
id={id}
|
||||||
|
|
@ -102,10 +100,6 @@ export const ZammadWrapper: FC<ZammadWrapperProps> = ({
|
||||||
const linkElement = document.querySelector(
|
const linkElement = document.querySelector(
|
||||||
`#${id}`,
|
`#${id}`,
|
||||||
) as HTMLIFrameElement;
|
) as HTMLIFrameElement;
|
||||||
|
|
||||||
console.log({ path });
|
|
||||||
console.log({ id });
|
|
||||||
console.log({ linkElement });
|
|
||||||
if (
|
if (
|
||||||
linkElement.contentDocument &&
|
linkElement.contentDocument &&
|
||||||
linkElement.contentDocument?.querySelector &&
|
linkElement.contentDocument?.querySelector &&
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
signOut({ callbackUrl: "/login" });
|
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 />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useState, useEffect } from "react";
|
import { FC, useState, useEffect, useActionState } from "react";
|
||||||
import { useFormState } from "react-dom";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Grid } from "@mui/material";
|
import { Grid } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
|
|
@ -44,7 +43,7 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [formState, formAction] = useFormState(
|
const [formState, formAction] = useActionState(
|
||||||
createTicketAction,
|
createTicketAction,
|
||||||
initialState,
|
initialState,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
|
||||||
columns={gridColumns}
|
columns={gridColumns}
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
getRowID={(row: any) => {
|
getRowID={(row: any) => {
|
||||||
console.log({ row });
|
|
||||||
return row.internalId;
|
return row.internalId;
|
||||||
}}
|
}}
|
||||||
buttons={
|
buttons={
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,18 @@ type ZammadOverviewProps = {
|
||||||
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
|
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
|
||||||
const [tickets, setTickets] = useState([]);
|
const [tickets, setTickets] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (typeof window !== "undefined") {
|
||||||
const hash = window?.location?.hash;
|
useEffect(() => {
|
||||||
|
const hash = window?.location?.hash;
|
||||||
|
|
||||||
if (hash) {
|
if (hash) {
|
||||||
const ticketID = hash.replace("#ticket/zoom/", "");
|
const ticketID = hash.replace("#ticket/zoom/", "");
|
||||||
if (ticketID && !isNaN(parseInt(ticketID, 10))) {
|
if (ticketID && !isNaN(parseInt(ticketID, 10))) {
|
||||||
redirect(`/tickets/${ticketID}`);
|
redirect(`/tickets/${ticketID}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [window?.location?.hash]);
|
||||||
}, [window?.location?.hash]);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ import { ZammadWrapper } from "app/(main)/_components/ZammadWrapper";
|
||||||
export const Setup: FC = () => {
|
export const Setup: FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useLayoutEffect(() => {
|
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);
|
setTimeout(() => router.push("/"), 4000);
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,6 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
const [agents, setAgents] = useState<any>();
|
const [agents, setAgents] = useState<any>();
|
||||||
const [pendingVisible, setPendingVisible] = useState(false);
|
const [pendingVisible, setPendingVisible] = useState(false);
|
||||||
|
|
||||||
const filteredStates =
|
|
||||||
ticketStates?.filter(
|
|
||||||
(state: any) => !["new", "merged", "removed"].includes(state.label),
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAgents = async () => {
|
const fetchAgents = async () => {
|
||||||
const groupID = formState?.values?.group?.split("/")?.pop();
|
const groupID = formState?.values?.group?.split("/")?.pop();
|
||||||
|
|
@ -96,7 +91,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
[name]: value,
|
[name]: value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const stateName = filteredStates?.find(
|
const stateName = ticketStates?.find(
|
||||||
(state: any) => state.id === formState.values.state,
|
(state: any) => state.id === formState.values.state,
|
||||||
)?.name;
|
)?.name;
|
||||||
setPendingVisible(stateName?.includes("pending") ?? false);
|
setPendingVisible(stateName?.includes("pending") ?? false);
|
||||||
|
|
@ -141,7 +136,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
|
||||||
label="State"
|
label="State"
|
||||||
formState={formState}
|
formState={formState}
|
||||||
updateFormState={updateFormState}
|
updateFormState={updateFormState}
|
||||||
getOptions={() => filteredStates}
|
getOptions={() => ticketStates}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,10 @@ export const getOverviewTicketsAction = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
if (name === "Recent") {
|
if (name === "Recent") {
|
||||||
const recent = await executeREST({ path: "/api/v1/recent_view" });
|
const recent = await executeREST({ path: "/api/v1/recent_view" });
|
||||||
|
const uniqueIDs = new Set(recent.map((rec: any) => rec.o_id));
|
||||||
for (const rec of recent) {
|
for (const id of uniqueIDs) {
|
||||||
const tkt = await executeREST({
|
const tkt = await executeREST({
|
||||||
path: `/api/v1/tickets/${rec.o_id}`,
|
path: `/api/v1/tickets/${id}`,
|
||||||
});
|
});
|
||||||
tickets.push({
|
tickets.push({
|
||||||
...tkt,
|
...tkt,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
import { getTicketQuery } from "app/_graphql/getTicketQuery";
|
||||||
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
|
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
|
||||||
import { createTicketMutation } from "app/_graphql/createTicketMutation";
|
import { createTicketMutation } from "app/_graphql/createTicketMutation";
|
||||||
|
|
@ -157,13 +156,14 @@ export const getTicketStatesAction = async () => {
|
||||||
const states = await executeREST({
|
const states = await executeREST({
|
||||||
path: "/api/v1/ticket_states",
|
path: "/api/v1/ticket_states",
|
||||||
});
|
});
|
||||||
|
console.log({ states });
|
||||||
const formattedStates =
|
const formattedStates =
|
||||||
states?.map((state: any) => ({
|
states?.map((state: any) => ({
|
||||||
value: `gid://zammad/Ticket::State/${state.id}`,
|
value: `gid://zammad/Ticket::State/${state.id}`,
|
||||||
label: state.name,
|
label: state.name,
|
||||||
|
disabled: ["new", "merged", "removed"].includes(state.name),
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
console.log({ formattedStates });
|
||||||
return formattedStates;
|
return formattedStates;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e.message);
|
console.error(e.message);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const ZammadLoginProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const response = await fetch("/api/v1/users/me", {
|
const response = await fetch("/api/v1/users/me", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Browser-Fingerprint": `${session.expires}`,
|
"X-Browser-Fingerprint": localStorage.getItem("fingerprint") || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export const authOptions: NextAuthOptions = {
|
||||||
},
|
},
|
||||||
providers,
|
providers,
|
||||||
session: {
|
session: {
|
||||||
maxAge: 7 * 24 * 60 * 60,
|
maxAge: 3 * 24 * 60 * 60,
|
||||||
},
|
},
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
import { getServerSession } from "app/_lib/authentication";
|
import { getServerSession } from "app/_lib/authentication";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
const getHeaders = async () => {
|
const getHeaders = async () => {
|
||||||
const userAgent = (await headers()).get("user-agent");
|
|
||||||
const allCookies = (await cookies()).getAll();
|
const allCookies = (await cookies()).getAll();
|
||||||
const hashedUserAgent = crypto
|
|
||||||
.createHash("sha256")
|
|
||||||
.update(userAgent)
|
|
||||||
.digest("hex");
|
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
const finalHeaders = {
|
const finalHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"X-Browser-Fingerprint": hashedUserAgent,
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
"X-CSRF-Token": session.user.zammadCsrfToken,
|
"X-CSRF-Token": session.user.zammadCsrfToken,
|
||||||
Cookie: allCookies
|
Cookie: allCookies
|
||||||
|
|
|
||||||
29
apps/link/app/api/logout/route.ts
Normal file
29
apps/link/app/api/logout/route.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,10 @@ const rewriteURL = (
|
||||||
const destinationURL = `${destinationBaseURL}/${path}`;
|
const destinationURL = `${destinationBaseURL}/${path}`;
|
||||||
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
||||||
const requestHeaders = new Headers(request.headers);
|
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-user");
|
||||||
requestHeaders.delete("x-forwarded-roles");
|
requestHeaders.delete("x-forwarded-roles");
|
||||||
requestHeaders.delete("connection");
|
requestHeaders.delete("connection");
|
||||||
|
|
@ -61,6 +65,12 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
|
||||||
return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers);
|
return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers);
|
||||||
} else if (zammadPaths.some((p) => request.nextUrl.pathname.startsWith(p))) {
|
} else if (zammadPaths.some((p) => request.nextUrl.pathname.startsWith(p))) {
|
||||||
return rewriteURL(request, linkBaseURL, zammadURL, headers);
|
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 {
|
} else {
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
||||||
|
|
@ -75,7 +85,7 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
|
||||||
object-src 'none';
|
object-src 'none';
|
||||||
base-uri 'self';
|
base-uri 'self';
|
||||||
form-action 'self';
|
form-action 'self';
|
||||||
frame-ancestors 'none';
|
frame-ancestors 'self';
|
||||||
upgrade-insecure-requests;
|
upgrade-insecure-requests;
|
||||||
`;
|
`;
|
||||||
const contentSecurityPolicyHeaderValue = cspHeader
|
const contentSecurityPolicyHeaderValue = cspHeader
|
||||||
|
|
@ -132,5 +142,5 @@ export default withAuth(checkRewrites, {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!ws|wss|api|_next/static|_next/image|favicon.ico).*)"],
|
matcher: ["/((?!ws|wss|_next/static|_next/image|favicon.ico).*)"],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,12 @@ export const Select: FC<SelectProps> = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{options.map((option: SelectOption) => (
|
{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}
|
{option.label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue