import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; import { users, userOrgs, orgs, idpOrg, idp, idpOidcConfig } from "@server/db"; import { eq, or, sql, and, isNotNull, inArray } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { UserType } from "@server/types/UserTypes"; const lookupBodySchema = z.strictObject({ identifier: z.string().min(1).toLowerCase() }); export type LookupUserResponse = { found: boolean; identifier: string; accounts: Array<{ userId: string; email: string | null; username: string; hasInternalAuth: boolean; orgs: Array<{ orgId: string; orgName: string; idps: Array<{ idpId: number; name: string; variant: string | null; }>; hasInternalAuth: boolean; }>; }>; }; // registry.registerPath({ // method: "post", // path: "/auth/lookup-user", // description: "Lookup user accounts by username or email and return available authentication methods.", // tags: [OpenAPITags.Auth], // request: { // body: lookupBodySchema // }, // responses: {} // }); export async function lookupUser( req: Request, res: Response, next: NextFunction ): Promise { try { const parsedBody = lookupBodySchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString() ) ); } const { identifier } = parsedBody.data; // Query users matching identifier (case-insensitive) // Match by username OR email const matchingUsers = await db .select({ userId: users.userId, email: users.email, username: users.username, type: users.type, passwordHash: users.passwordHash, idpId: users.idpId }) .from(users) .where( or( sql`LOWER(${users.username}) = ${identifier}`, sql`LOWER(${users.email}) = ${identifier}` ) ); if (!matchingUsers || matchingUsers.length === 0) { return response(res, { data: { found: false, identifier, accounts: [] }, success: true, error: false, message: "No accounts found", status: HttpCode.OK }); } // Get unique user IDs const userIds = [...new Set(matchingUsers.map((u) => u.userId))]; // Get all org memberships for these users const orgMemberships = await db .select({ userId: userOrgs.userId, orgId: userOrgs.orgId, orgName: orgs.name }) .from(userOrgs) .innerJoin(orgs, eq(orgs.orgId, userOrgs.orgId)) .where(inArray(userOrgs.userId, userIds)); // Get unique org IDs const orgIds = [...new Set(orgMemberships.map((m) => m.orgId))]; // Get all IdPs for these orgs const orgIdps = orgIds.length > 0 ? await db .select({ orgId: idpOrg.orgId, idpId: idp.idpId, idpName: idp.name, variant: idpOidcConfig.variant }) .from(idpOrg) .innerJoin(idp, eq(idp.idpId, idpOrg.idpId)) .innerJoin( idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId) ) .where(inArray(idpOrg.orgId, orgIds)) : []; // Build response structure const accounts: LookupUserResponse["accounts"] = []; for (const user of matchingUsers) { const hasInternalAuth = user.type === UserType.Internal && user.passwordHash !== null; // Get orgs for this user const userOrgMemberships = orgMemberships.filter( (m) => m.userId === user.userId ); // Deduplicate orgs (user might have multiple memberships in same org) const uniqueOrgs = new Map(); for (const membership of userOrgMemberships) { if (!uniqueOrgs.has(membership.orgId)) { uniqueOrgs.set(membership.orgId, membership); } } const orgsData = Array.from(uniqueOrgs.values()).map((membership) => { // Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP // Only show IdPs where the user's idpId matches // Internal users don't have an idpId, so they won't see any IdPs const orgIdpsList = orgIdps .filter((idp) => { if (idp.orgId !== membership.orgId) { return false; } // Only show IdPs where the user (with exact identifier) is authenticated via that IdP // This means user.idpId must match idp.idpId if (user.idpId !== null && user.idpId === idp.idpId) { return true; } return false; }) .map((idp) => ({ idpId: idp.idpId, name: idp.idpName, variant: idp.variant })); // Check if user has internal auth for this org // User has internal auth if they have an internal account type const orgHasInternalAuth = hasInternalAuth; return { orgId: membership.orgId, orgName: membership.orgName, idps: orgIdpsList, hasInternalAuth: orgHasInternalAuth }; }); accounts.push({ userId: user.userId, email: user.email, username: user.username, hasInternalAuth, orgs: orgsData }); } return response(res, { data: { found: true, identifier, accounts }, success: true, error: false, message: "User lookup completed", status: HttpCode.OK }); } catch (error) { logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } }