import { ApolloClient, InMemoryCache } from "@apollo/client"; import { Client } from "ldapts"; import { gql } from "./__generated__"; import sharp from "sharp"; import { BerWriter } from "asn1"; import { User } from "@prisma/client"; type LLDAPAuthResponse = { token: string; refreshToken: string; }; type LLDAPRefreshResponse = { token: string; }; export type UserInfo = { username: string; displayName: string; email: string; avatar?: string; }; let ldapClient: Client | null = null; async function getLdapClient() { if (ldapClient === null) { ldapClient = new Client({ url: `ldap://${process.env.LDAP_HOST}:3890` }); const full = `uid=${process.env.LDAP_BIND_USER},ou=people,${process.env.LDAP_DC}`; await ldapClient.bind(full, process.env.LDAP_BIND_PASSWORD); } return ldapClient; } let authResponse: LLDAPAuthResponse | null = null; async function regenAuthToken() { if (authResponse !== null) { const url = `http://${process.env.LDAP_HOST}:17170/auth/refresh`; const req = await fetch(url, { headers: { "Refresh-Token": authResponse.refreshToken } }); const res: LLDAPRefreshResponse = await req.json(); authResponse.token = res.token; } else { const url = `http://${process.env.LDAP_HOST}:17170/auth/simple/login`; const req = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: process.env.LDAP_BIND_USER, password: process.env.LDAP_BIND_PASSWORD }) }); authResponse = await req.json(); } // valid for one day, so refresh every 12 hours setTimeout(regenAuthToken, 12 * 60 * 60 * 1000); } async function getAuthToken() { if (authResponse === null) await regenAuthToken(); return authResponse!.token; } let graphQLClient: ApolloClient | null = null; let graphQLCache = new InMemoryCache(); let graphQLAuthToken: string | null = null; async function getGraphQLClient() { if (authResponse === null) { await getAuthToken(); graphQLAuthToken = authResponse!.token; } // We keep track of the auth token we used in the client, so we can // recreate it when it expires/refreshes if (graphQLClient === null || graphQLAuthToken !== authResponse!.token) { graphQLClient = new ApolloClient({ uri: `http://${process.env.LDAP_HOST}:17170/api/graphql`, cache: graphQLCache, headers: { Authorization: `Bearer ${authResponse!.token}` } }); } return graphQLClient; } export async function getUsers() { const client = await getGraphQLClient(); const query = await client.query({ query: gql(` query GetUsers { users { id } } `) }); return query.data.users; } async function ensureJpg(avatar: Buffer) { const img = await sharp(avatar).toFormat("jpeg").resize(512, 512); const buf = await img.toBuffer(); return buf.toString("base64"); } export async function createUser( username: string, displayName: string, email: string, avatar?: Buffer ) { const fixedAvatar = avatar != null ? await ensureJpg(avatar) : null; const client = await getGraphQLClient(); const mutation = await client.mutate({ mutation: gql(` mutation CreateUser($user: CreateUserInput!) { createUser(user: $user) { id } } `), variables: { user: { id: username, displayName, email, avatar: fixedAvatar } } }); } export async function setPassword(user: string, password: string) { const client = await getLdapClient(); const dn = `uid=${user},ou=people,${process.env.LDAP_DC}`; // god bless random stackoverflow user // https://stackoverflow.com/questions/65745679/how-do-i-pass-parameters-to-the-ldapjs-exop-function const CTX_SPECIFIC_CLASS = 0b10 << 6; const writer = new BerWriter(); writer.startSequence(); writer.writeString(dn, CTX_SPECIFIC_CLASS | 0); writer.writeString(password, CTX_SPECIFIC_CLASS | 2); writer.endSequence(); await client.exop("1.3.6.1.4.1.4203.1.11.1", writer.buffer); } export async function validateUser(username: string, password: string) { const client = new Client({ url: `ldap://${process.env.LDAP_HOST}:3890` }); try { const dn = `uid=${username},ou=people,${process.env.LDAP_DC}`; await client.bind(dn, password); await client.unbind(); return true; } catch (e) { return false; } } export async function checkUserExists(username: string) { return (await getUsers()).find( (u) => u.id.toLowerCase() === username.toLowerCase() ); } export async function getUserInfo(user: User) { if (user.username === null) return null; const client = await getGraphQLClient(); const mutation = await client.query({ query: gql(` query GetUser($userId: String!) { user(userId: $userId) { id email displayName avatar } } `), variables: { userId: user.username } }); const mutationAvatar = mutation.data.user.avatar; const avatar = mutationAvatar ? `data:image/jpeg;base64,${mutationAvatar}` : undefined; const userInfo: UserInfo = { username: mutation.data.user.id, displayName: mutation.data.user.displayName, email: mutation.data.user.email, avatar }; return userInfo; } export async function updateUser( user: User, displayName?: string, email?: string, avatar?: Buffer ) { if (user.username === null) return; const client = await getGraphQLClient(); const mutation = await client.mutate({ mutation: gql(` mutation UpdateUser($user: UpdateUserInput!) { updateUser(user: $user) { ok } } `), variables: { user: { id: user.username, displayName, email, avatar: avatar ? await ensureJpg(avatar) : undefined } } }); }