gluestick/src/ldap.ts

258 lines
6.0 KiB
TypeScript

import { ApolloClient, InMemoryCache } from "@apollo/client";
import { Client } from "ldapts";
import { gql } from "./__generated__";
import { BerWriter } from "asn1";
import { User } from "@prisma/client";
import { ensureJpg } from "@/image";
import prisma from "./prisma";
export type LLDAPAuthResponse = {
token: string;
refreshToken: string;
};
type LLDAPRefreshResponse = {
token: string;
};
export type UserInfo = {
username: string;
displayName: string;
email: string;
avatar?: string;
discordId?: string;
githubId?: string;
};
async function getLdapClient() {
if (global.ldapClient == null) {
global.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 global.ldapClient.bind(full, process.env.LDAP_BIND_PASSWORD);
}
return global.ldapClient;
}
async function regenAuthToken() {
if (global.authResponse != null) {
const url = `http://${process.env.LDAP_HOST}:17170/auth/refresh`;
const req = await fetch(url, {
headers: {
"Refresh-Token": global.authResponse.refreshToken
}
});
const res: LLDAPRefreshResponse = await req.json();
global.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
})
});
global.authResponse = await req.json();
}
// valid for one day, so refresh every 12 hours
setTimeout(regenAuthToken, 12 * 60 * 60 * 1000);
}
async function getAuthToken() {
if (global.authResponse == null) await regenAuthToken();
return global.authResponse!.token;
}
async function getGraphQLClient() {
if (global.authResponse == null) {
await getAuthToken();
}
// Remake the client every time because Apollo caching is fucking stupid
let graphQLClient = new ApolloClient({
uri: `http://${process.env.LDAP_HOST}:17170/api/graphql`,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: "no-cache",
errorPolicy: "ignore"
},
query: {
fetchPolicy: "no-cache",
errorPolicy: "all"
}
},
headers: {
Authorization: `Bearer ${global.authResponse!.token}`
}
});
// whoever designed to cache this shit is FUCKING STUPID
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;
}
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) {
const users = await getUsers();
return users.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 dbUser = await prisma.user.findFirst({
where: {
username: user.username
},
include: {
discordAuth: true,
githubAuth: true
}
});
const userInfo: UserInfo = {
username: mutation.data.user.id,
displayName: mutation.data.user.displayName,
email: mutation.data.user.email,
discordId: dbUser?.discordAuth?.id,
githubId: dbUser?.githubAuth?.id?.toString(),
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
}
}
});
}