- Create new @link-stack/logger package wrapping Pino for structured logging - Replace all console.log/error/warn statements across the monorepo - Configure environment-aware logging (pretty-print in dev, JSON in prod) - Add automatic redaction of sensitive fields (passwords, tokens, etc.) - Remove dead commented-out logger file from bridge-worker - Follow Pino's standard argument order (context object first, message second) - Support log levels via LOG_LEVEL environment variable - Export TypeScript types for better IDE support This provides consistent, structured logging across all applications and packages, making debugging easier and production logs more parseable.
173 lines
4.6 KiB
TypeScript
173 lines
4.6 KiB
TypeScript
import type {
|
|
GetServerSidePropsContext,
|
|
NextApiRequest,
|
|
NextApiResponse,
|
|
} from "next";
|
|
import {
|
|
NextAuthOptions,
|
|
getServerSession as internalGetServerSession,
|
|
} from "next-auth";
|
|
import Google from "next-auth/providers/google";
|
|
import Credentials from "next-auth/providers/credentials";
|
|
import Apple from "next-auth/providers/apple";
|
|
import { Redis } from "ioredis";
|
|
import AzureADProvider from "next-auth/providers/azure-ad";
|
|
import { createLogger } from "@link-stack/logger";
|
|
|
|
const logger = createLogger('link-authentication');
|
|
|
|
const headers = { Authorization: `Token ${process.env.ZAMMAD_API_TOKEN}` };
|
|
|
|
const fetchRoles = async () => {
|
|
const url = `${process.env.ZAMMAD_URL}/api/v1/roles`;
|
|
const res = await fetch(url, { headers });
|
|
const roles = await res.json();
|
|
const formattedRoles = roles.reduce((acc: any, role: any) => {
|
|
acc[role.id] = role.name;
|
|
return acc;
|
|
}, {});
|
|
return formattedRoles;
|
|
};
|
|
|
|
const fetchUser = async (email: string) => {
|
|
const url = `${process.env.ZAMMAD_URL}/api/v1/users/search?query=login:${email}&limit=1`;
|
|
const res = await fetch(url, { headers });
|
|
const users = await res.json();
|
|
const user = users?.[0];
|
|
|
|
return user;
|
|
};
|
|
|
|
const getUserRoles = async (email: string) => {
|
|
try {
|
|
const user = await fetchUser(email);
|
|
if (!user) {
|
|
return [];
|
|
}
|
|
const allRoles = await fetchRoles();
|
|
const roles = user.role_ids.map((roleID: number) => {
|
|
const role = allRoles[roleID];
|
|
return role ? role.toLowerCase().replace(" ", "_") : null;
|
|
});
|
|
return roles.filter((role: string) => role !== null);
|
|
} catch (e) {
|
|
logger.error({ error: e, email }, 'Failed to get user roles');
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const login = async (email: string, password: string) => {
|
|
const url = `${process.env.ZAMMAD_URL}/api/v1/users/me`;
|
|
const authorization =
|
|
"Basic " + Buffer.from(email + ":" + password).toString("base64");
|
|
const res = await fetch(url, {
|
|
headers: {
|
|
authorization,
|
|
},
|
|
});
|
|
const user = await res.json();
|
|
|
|
if (user && !user.error && user.id) {
|
|
return user;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const providers = [];
|
|
|
|
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
providers.push(
|
|
Google({
|
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
}),
|
|
);
|
|
} else if (process.env.APPLE_CLIENT_ID && process.env.APPLE_CLIENT_SECRET) {
|
|
providers.push(
|
|
Apple({
|
|
clientId: process.env.APPLE_CLIENT_ID,
|
|
clientSecret: process.env.APPLE_CLIENT_SECRET,
|
|
}),
|
|
);
|
|
} else if (
|
|
process.env.AZURE_AD_CLIENT_ID &&
|
|
process.env.AZURE_AD_CLIENT_SECRET &&
|
|
process.env.AZURE_AD_TENANT_ID
|
|
) {
|
|
providers.push(
|
|
AzureADProvider({
|
|
clientId: process.env.AZURE_AD_CLIENT_ID,
|
|
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
|
tenantId: process.env.AZURE_AD_TENANT_ID,
|
|
}),
|
|
);
|
|
} else {
|
|
providers.push(
|
|
Credentials({
|
|
name: "Zammad",
|
|
credentials: {
|
|
email: { label: "Email", type: "text" },
|
|
password: { label: "Password", type: "password" },
|
|
},
|
|
async authorize(credentials) {
|
|
const user = await login(credentials.email, credentials.password);
|
|
if (user) {
|
|
return user;
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
export const authOptions: NextAuthOptions = {
|
|
pages: {
|
|
signIn: "/link/login",
|
|
error: "/link/login",
|
|
signOut: "/link/logout",
|
|
},
|
|
providers,
|
|
session: {
|
|
maxAge: 3 * 24 * 60 * 60,
|
|
},
|
|
secret: process.env.NEXTAUTH_SECRET,
|
|
callbacks: {
|
|
signIn: async ({ user }) => {
|
|
const roles = (await getUserRoles(user.email)) ?? [];
|
|
return roles.includes("admin") || roles.includes("agent");
|
|
},
|
|
session: async ({ session, token }) => {
|
|
// const redis = new Redis(process.env.REDIS_URL);
|
|
// const isInvalidated = await redis.get(`invalidated:${token.sub}`);
|
|
// if (isInvalidated) {
|
|
// return null;
|
|
// }
|
|
// @ts-ignore
|
|
session.user.roles = token.roles ?? [];
|
|
// @ts-ignore
|
|
session.user.zammadCsrfToken = token.zammadCsrfToken;
|
|
|
|
return session;
|
|
},
|
|
jwt: async ({ token, user, trigger, session }) => {
|
|
if (user) {
|
|
token.roles = (await getUserRoles(user.email)) ?? [];
|
|
}
|
|
|
|
if (session && trigger === "update") {
|
|
token.zammadCsrfToken = session.zammadCsrfToken;
|
|
}
|
|
|
|
return token;
|
|
},
|
|
},
|
|
};
|
|
|
|
export const getServerSession = (
|
|
...args:
|
|
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
|
|
| [NextApiRequest, NextApiResponse]
|
|
| []
|
|
) => internalGetServerSession(...args, authOptions);
|