mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-30 11:19:12 +01:00
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
import { cookies } from "next/headers";
|
|
import dayjs from "dayjs";
|
|
import type { NextAuthConfig } from "next-auth";
|
|
|
|
import { and, eq, inArray } from "@homarr/db";
|
|
import type { Database } from "@homarr/db";
|
|
import { groupMembers, groups, users } from "@homarr/db/schema";
|
|
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
|
import { logger } from "@homarr/log";
|
|
|
|
import { env } from "./env";
|
|
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
|
|
|
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
|
return async ({ user, profile }) => {
|
|
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
|
|
if (!user.id) throw new Error("User ID is missing");
|
|
|
|
const dbUser = await db.query.users.findFirst({
|
|
where: eq(users.id, user.id),
|
|
columns: {
|
|
name: true,
|
|
colorScheme: true,
|
|
},
|
|
});
|
|
|
|
if (!dbUser) throw new Error("User not found");
|
|
|
|
const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
|
|
// Groups from oidc provider are provided from the profile, it's not typed.
|
|
if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) {
|
|
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
|
|
await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
|
|
}
|
|
|
|
// In ldap-authroization we return the groups from ldap, it's not typed.
|
|
if ("groups" in user && Array.isArray(user.groups)) {
|
|
logger.debug(`Using profile groups: ${JSON.stringify(user.groups)}`);
|
|
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
|
|
}
|
|
await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);
|
|
|
|
if (dbUser.name !== user.name) {
|
|
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
|
logger.info(
|
|
`Username for user of credentials provider has changed. user=${user.id} old=${dbUser.name} new=${user.name}`,
|
|
);
|
|
}
|
|
|
|
if (profile) {
|
|
const profileUsername = extractProfileName(profile);
|
|
if (!profileUsername) {
|
|
throw new Error(`OIDC provider did not return a name properties='${Object.keys(profile).join(",")}'`);
|
|
}
|
|
|
|
if (dbUser.name !== profileUsername) {
|
|
await db.update(users).set({ name: profileUsername }).where(eq(users.id, user.id));
|
|
logger.info(
|
|
`Username for user of oidc provider has changed. user=${user.id} old='${dbUser.name}' new='${profileUsername}'`,
|
|
);
|
|
}
|
|
}
|
|
|
|
logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);
|
|
|
|
// We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
|
|
(await cookies()).set(colorSchemeCookieKey, dbUser.colorScheme, {
|
|
path: "/",
|
|
expires: dayjs().add(1, "year").toDate(),
|
|
});
|
|
};
|
|
};
|
|
|
|
const addUserToEveryoneGroupIfNotMemberAsync = async (db: Database, userId: string) => {
|
|
const dbEveryoneGroup = await db.query.groups.findFirst({
|
|
where: eq(groups.name, everyoneGroup),
|
|
with: {
|
|
members: {
|
|
where: eq(groupMembers.userId, userId),
|
|
},
|
|
},
|
|
});
|
|
|
|
if (dbEveryoneGroup?.members.length === 0) {
|
|
await db.insert(groupMembers).values({
|
|
userId,
|
|
groupId: dbEveryoneGroup.id,
|
|
});
|
|
logger.info(`Added user to everyone group. user=${userId}`);
|
|
}
|
|
};
|
|
|
|
const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: string, externalGroups: string[]) => {
|
|
const ignoredGroups = [everyoneGroup];
|
|
const dbGroupMembers = await db.query.groupMembers.findMany({
|
|
where: eq(groupMembers.userId, userId),
|
|
with: {
|
|
group: { columns: { name: true } },
|
|
},
|
|
});
|
|
|
|
/**
|
|
* The below groups are those groups the user is part of in the external system, but not in Homarr.
|
|
* So he has to be added to those groups.
|
|
*/
|
|
const missingExternalGroupsForUser = externalGroups.filter(
|
|
(externalGroup) => !dbGroupMembers.some(({ group }) => group.name === externalGroup),
|
|
);
|
|
|
|
if (missingExternalGroupsForUser.length > 0) {
|
|
logger.debug(
|
|
`Homarr does not have the user in certain groups. user=${userId} count=${missingExternalGroupsForUser.length}`,
|
|
);
|
|
|
|
const groupIds = await db.query.groups.findMany({
|
|
columns: {
|
|
id: true,
|
|
},
|
|
where: inArray(groups.name, missingExternalGroupsForUser),
|
|
});
|
|
|
|
logger.debug(`Homarr has found groups in the database user is not in. user=${userId} count=${groupIds.length}`);
|
|
|
|
if (groupIds.length > 0) {
|
|
await db.insert(groupMembers).values(
|
|
groupIds.map((group) => ({
|
|
userId,
|
|
groupId: group.id,
|
|
})),
|
|
);
|
|
|
|
logger.info(`Added user to groups successfully. user=${userId} count=${groupIds.length}`);
|
|
} else {
|
|
logger.debug(`User is already in all groups of Homarr. user=${userId}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The below groups are those groups the user is part of in Homarr, but not in the external system and not ignored.
|
|
* So he has to be removed from those groups.
|
|
*/
|
|
const groupsUserIsNoLongerMemberOfExternally = dbGroupMembers.filter(
|
|
({ group }) => !externalGroups.concat(ignoredGroups).includes(group.name),
|
|
);
|
|
|
|
if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
|
|
logger.debug(
|
|
`Homarr has the user in certain groups that LDAP does not have. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
|
|
);
|
|
|
|
await db.delete(groupMembers).where(
|
|
and(
|
|
eq(groupMembers.userId, userId),
|
|
inArray(
|
|
groupMembers.groupId,
|
|
groupsUserIsNoLongerMemberOfExternally.map(({ groupId }) => groupId),
|
|
),
|
|
),
|
|
);
|
|
|
|
logger.info(
|
|
`Removed user from groups successfully. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
|
|
);
|
|
}
|
|
};
|