feat: Add base to project

Signed-off-by: Avior <git@avior.me>
This commit is contained in:
2024-05-16 16:45:50 +02:00
parent 9b2d412a9e
commit f50ec828fb
36 changed files with 5248 additions and 1698 deletions

View File

@ -0,0 +1,193 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { objectClone, objectLoop, objectMap, objectRemap } from '@dzeio/object-util'
import ldap from 'ldapjs'
import type DaoAdapter from 'models/DaoAdapter'
import type { DBPull } from 'models/DaoAdapter'
import { type Query } from 'models/Query'
import Schema, { type Implementation, type Model } from 'models/Schema'
type LDAPFields = 'uid' | 'mail' | 'givenname' | 'sn' | 'jpegPhoto' | 'password'
export default class LDAPAdapter<T extends Model> implements DaoAdapter<T> {
private reverseReference: Partial<Record<LDAPFields | string, keyof T>> = {}
private attributes: Array<LDAPFields | string> = []
public constructor(
public readonly schema: Schema<T>,
public readonly options: {
url: string
dnSuffix: string
adminUsername: string
adminPassword: string
fieldsCorrespondance?: Partial<Record<keyof T, LDAPFields | string>>
}
) {
objectLoop(options.fieldsCorrespondance ?? {}, (value, key) => {
this.reverseReference[value] = key
this.attributes.push(value)
})
}
// TODO: make it clearer what it does
public async create(_obj: Partial<Implementation<T>>): Promise<Implementation<T> | null> {
throw new Error('not implemented')
}
// eslint-disable-next-line complexity
public async read(query?: Query<Implementation<T>> | undefined): Promise<DBPull<T>> {
const passwordField = this.options.fieldsCorrespondance?.password ?? 'password'
const doLogin = !!query?.[passwordField]
const emptyResult = {
rows: 0,
rowsTotal: 0,
page: 1,
pageTotal: 0,
data: []
}
if (!query) {
return emptyResult
}
// console.log(await this.ldapFind({mail: 'f.bouillon@aptatio.com'}))
const userdn = objectMap(query, (value, key) => `${(this.options.fieldsCorrespondance as any)[key] ?? key}=${value}`)
?.filter((it) => it.slice(0, it.indexOf('=')) !== passwordField)
?.join(',')
if (!doLogin) {
const client = await this.bind(`cn=${this.options.adminUsername},${this.options.dnSuffix}`, this.options.adminPassword)
// @ts-expect-error nique ta mere
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const results = (await this.ldapFind(client, objectMap(query, (value, key) => [key as string, '', value as string])
.map((it) => ({key: it[0] as LDAPFields, value: it[2]}))!
)).map((it) => this.schema.parse(
objectRemap(it, (value, key) => ({key: this.reverseReference[key as string] as string, value: value}))
)).filter((it): it is Implementation<T> => !!it)
return {
rows: results.length,
rowsTotal: results.length,
page: 1,
pageTotal: 1,
data: results
}
}
try {
const clone = objectClone(query)
delete clone.password
const res = await this.read(clone)
const user = res.data[0]
if (!user) {
return emptyResult
}
const password = query.password as string ?? ''
const client = await this.bind(`cn=${user[this.reverseReference.uid as keyof typeof user]!},${this.options.dnSuffix}`, password)
// @ts-expect-error nique x2
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const results = (await this.ldapFind(client, objectMap(clone, (value, key) => ({key: key as keyof LDAPFields, value: value}))
)).map((it) => this.schema.parse(
objectRemap(it, (value, key) => ({key: this.reverseReference[key as string] as string, value: value}))
)).filter((it): it is Implementation<T> => !!it)
if (results.length !== 1) {
return emptyResult
}
return {
rows: results.length,
rowsTotal: results.length,
page: 1,
pageTotal: 1,
data: results
}
} catch (e) {
console.log('error, user not found', e)
return emptyResult
}
}
public async update(_obj: Partial<Implementation<T>>): Promise<Implementation<T> | null> {
throw new Error('not implemented')
}
public async patch(_id: string, _obj: Partial<Implementation<T>>): Promise<Implementation<T> | null> {
throw new Error('not implemented')
}
public async delete(_obj: Partial<Implementation<T>>): Promise<boolean> {
throw new Error('not implemented')
}
private bind(dn: string, password: string): Promise<ldap.Client> {
const client = ldap.createClient({
url: this.options.url
})
return new Promise<ldap.Client>((res, rej) => {
client.on('connect', () => {
client.bind(dn, password, (err) => {
if (err) {
console.error('error binding as', dn, err)
client.unbind()
return
}
console.log('binded as', dn)
res(client)
})
})
.on('timeout', (err) => rej(err))
.on('connectTimeout', (err) => rej(err))
.on('error', (err) => rej(err))
.on('connectError', (err) => rej(err))
})
}
private async ldapFind(client: ldap.Client, filters: Array<{key: LDAPFields, value: string}>): Promise<Array<Record<LDAPFields, string | Array<string> | undefined>>> {
if (filters.length === 0) {
return []
}
const firstFilter = filters.shift()!
return new Promise<Array<Record<LDAPFields, string | Array<string> | undefined>>>((res, rej) => {
const users: Array<Record<LDAPFields, string | Array<string> | undefined>> = []
client.search(
this.options.dnSuffix, {
filter: new ldap.EqualityFilter({
attribute: firstFilter.key as any,
value: firstFilter.value,
}),
scope: 'sub',
attributes: this.attributes
}, (err, search) => {
if (err) {
rej(err)
}
// console.log('search', search, err)
search.on('searchEntry', (entry) => {
users.push(this.parseUser(entry))
}).on('error', (err2) => {
rej(err2)
client.unbind()
console.error('error in search lol', err2)
}).on('end', () => {
console.log(users)
res(users)
client.unbind()
})
}
)
})
}
private parseUser(usr: ldap.SearchEntry): Record<LDAPFields, string | Array<string> | undefined> {
const user: Record<string, string | Array<string> | undefined> = { dn: usr.objectName ?? undefined }
usr.attributes.forEach((attribute) => {
user[attribute.type] =
attribute.values.length === 1 ? attribute.values[0] : attribute.values
})
return user
}
}