Refactoring

This commit is contained in:
Douglas Barone 2023-06-15 10:00:54 -04:00
parent 26771cecdc
commit c8557b9442
11 changed files with 169 additions and 141 deletions

View File

@ -1,12 +1,14 @@
import jwt from 'jsonwebtoken'
import { prisma } from './prisma.js'
import { ldapClient } from './ldapClient.js'
import { LdapController } from './controllers/LdapController.js'
import { UserController } from './controllers/UserController.js'
const JWT_SECRET = process.env.JWT_SECRET || 'secret'
export async function login(username: string, password: string) {
await ldapClient.authenticate(username, password)
const ldap = new LdapController()
await ldap.authenticate(username, password)
await UserController.importUser(username)

View File

@ -0,0 +1 @@
export class LoginController {}

View File

@ -0,0 +1,85 @@
import { Client } from 'ldapts'
const DOMAIN = process.env.AD_DOMAIN || 'IFMS'
const DN = process.env.AD_DN || 'DC=ifms,DC=edu,DC=br'
const BIND_USER = process.env.AD_BIND_USER || ''
const BIND_PASSWD = process.env.AD_BIND_PASSWORD || ''
interface LdapClientInterface extends Client {
authenticate(username: string, password: string): Promise<LdapUser | null>
getUser(username: string): Promise<any>
}
type LdapUser = {
username: string
mail: string | null
displayName: string
thumbnailPhoto: string | null
}
export class LdapController extends Client implements LdapClientInterface {
private static instance: LdapController
constructor() {
if (LdapController.instance) return LdapController.instance
super({
url: `ldap://${process.env.AD_HOST}`
})
LdapController.instance = this
}
/**
* Execute a function using the ldap admin credentials.
*/
private async adminBondOperation(cb: () => Promise<any>) {
try {
await this.bind(`${DOMAIN}\\${BIND_USER}`, BIND_PASSWD)
return await cb()
} catch (error: any) {
throw new Error(`Error doing an Admin-bonded LDAP operation. ${error}`)
} finally {
await this.unbind()
}
}
async getUser(username: string) {
return await this.adminBondOperation(async () => {
const { searchEntries } = await this.search(DN, {
scope: 'sub',
filter: `(sAMAccountName=${username})`,
attributes: ['mail', 'sAMAccountName', 'displayName', 'thumbnailPhoto'],
explicitBufferAttributes: ['thumbnailPhoto']
})
if (!searchEntries.length)
throw new Error('User not found on LDAP server.')
const { sAMAccountName, displayName, mail, thumbnailPhoto } =
searchEntries[0]
const ldapUser: LdapUser = {
username: sAMAccountName.toString(),
displayName: displayName.toString(),
mail: mail.toString(),
thumbnailPhoto: `data:image/png;base64,${Buffer.from(
thumbnailPhoto as Buffer
).toString('base64')}`
}
return ldapUser
})
}
async authenticate(username: string, password: string) {
await this.bind(`${DOMAIN}\\${username}`, password)
const user = await this.getUser(username)
await this.unbind()
return user
}
}

View File

@ -1,9 +1,13 @@
import { ldapClient } from '../ldapClient.js'
import { LdapController } from '../controllers/LdapController.js'
import { prisma } from '../prisma.js'
export class UserController {
static async importUser(username: string) {
const user = await ldapClient.getUser(username)
const ldap = new LdapController()
const user = await ldap.getUser(username)
if (!user) throw new Error('User not found!')
return await prisma.user.upsert({
where: { username: user.username },
@ -11,19 +15,4 @@ export class UserController {
create: user
})
}
static async getUser(username: string) {
const user = await prisma.user.findUnique({
where: { username }
})
try {
if (!user) return await UserController.importUser(username)
else UserController.importUser(username)
return user
} catch (error: any) {
throw new Error('User not found!' + error.message)
}
}
}

View File

@ -0,0 +1,24 @@
import { UserController } from './UserController.js'
import { prisma } from '../prisma.js'
import { Request, Response } from 'express'
export class UserRouteController {
static async get(req: Request, res: Response) {
const { username } = req.params
if (!username) return res.status(400).json({ error: 'Missing username' })
try {
const user = await prisma.user.findUnique({
where: { username }
})
if (!user) return await UserController.importUser(username)
else UserController.importUser(username)
res.json(user)
} catch (error: any) {
res.status(500).json({ error: error.message })
}
}
}

View File

@ -2,15 +2,14 @@ import 'dotenv/config'
import express, { Request, Response } from 'express'
import bodyParser from 'body-parser'
import {
authenticatedMiddleware,
hasRolesMiddleware
} from './middleware/authorization.js'
import { injectUserMiddleware } from './middleware/injectUser.js'
import { injectUserMiddleware } from './middleware/injectUserMiddleware.js'
import { authMiddleware } from './middleware/authMiddleware.js'
import { hasRolesMiddleware } from './middleware/hasRolesMiddleware.js'
import { RequestWithUser } from './types.js'
import { login } from './authentication.js'
import { UserController } from './controllers/UserController.js'
import { UserRouteController } from './controllers/UserRouteController.js'
const app = express()
@ -44,15 +43,15 @@ app.post('/api/login', async (req: Request, res: Response) => {
app.get(
'/api/me',
authenticatedMiddleware,
authMiddleware,
async (req: RequestWithUser, res: Response) => res.json(req.user)
)
app.get(
'/api/protected',
authenticatedMiddleware,
authMiddleware,
async (req: RequestWithUser, res: Response) => {
res.send('Hello protected world! ' + req.user?.displayName)
res.json('Hello protected world! ' + req.user?.displayName)
}
)
@ -60,18 +59,14 @@ app.get(
'/api/admin',
await hasRolesMiddleware(['ADMIN']),
async (req: RequestWithUser, res: Response) => {
res.send('Hello Admin!' + req.user?.username)
res.json('Hello Admin!' + req.user?.username)
}
)
app.get(
'/api/user/:username',
await hasRolesMiddleware(['ADMIN']),
async (req: Request, res: Response) => {
const { username } = req.params
res.json(await UserController.getUser(username))
}
UserRouteController.get
)
// Start server

View File

@ -1,65 +0,0 @@
import { Client } from 'ldapts'
const DOMAIN = process.env.AD_DOMAIN || 'IFMS'
const DN = process.env.AD_DN || 'DC=ifms,DC=edu,DC=br'
const BIND_USER = process.env.AD_BIND_USER || ''
const BIND_PASSWD = process.env.AD_BIND_PASSWORD || ''
interface LdapClient extends Client {
adminBind: () => Promise<void>
getUser(username: string): Promise<any>
authenticate(username: string, password: string): Promise<any>
}
export const ldapClient = new Client({
url: `ldap://${process.env.AD_HOST}`
}) as LdapClient
ldapClient.adminBind = async () => {
await ldapClient.bind(`${DOMAIN}\\${BIND_USER}`, BIND_PASSWD)
}
ldapClient.getUser = async (username: string) => {
await ldapClient.adminBind()
const { searchEntries } = await ldapClient.search(DN, {
scope: 'sub',
filter: `(sAMAccountName=${username})`,
attributes: [
'cn',
'mail',
'sAMAccountName',
'displayName',
'thumbnailPhoto'
],
explicitBufferAttributes: ['thumbnailPhoto']
})
await ldapClient.unbind()
const { sAMAccountName, displayName, mail, thumbnailPhoto } = searchEntries[0]
const user = {
username: sAMAccountName.toString(),
displayName: displayName.toString(),
mail: mail.toString(),
thumbnailPhoto: `data:image/png;base64,${Buffer.from(
thumbnailPhoto as Buffer
).toString('base64')}`
}
return user
}
ldapClient.authenticate = async (username: string, password: string) => {
try {
await ldapClient.bind(`${DOMAIN}\\${username}`, password)
} catch (error: any) {
if (error.code === 49) {
throw new Error('Invalid username or password')
}
throw error
} finally {
await ldapClient.unbind()
}
}

View File

@ -0,0 +1,19 @@
import { Response, NextFunction } from 'express'
import { RequestWithUser } from '../types.js'
export async function authMiddleware(
req: RequestWithUser,
res: Response,
next: NextFunction
) {
try {
if (!req.user) {
res.status(401).json({ error: 'Must be logged in' })
return
}
next()
} catch (error: any) {
res.status(401).json({ error: error.message })
}
}

View File

@ -1,42 +0,0 @@
import { Request, Response, NextFunction } from 'express'
import { authenticate } from '../authentication.js'
import { RequestWithUser } from '../types.js'
import { Role } from '@prisma/client'
export async function authenticatedMiddleware(
req: RequestWithUser,
res: Response,
next: NextFunction
) {
try {
if (!req.user) {
res.status(401).json({ error: 'Must be logged in' })
return
}
next()
} catch (error: any) {
res.status(401).json({ error: error.message })
}
}
export async function hasRolesMiddleware(roles: Role[]) {
return async function (
req: RequestWithUser,
res: Response,
next: NextFunction
) {
try {
const userRoles = req.user?.roles
if (userRoles === undefined) {
throw new Error('User has no roles')
}
if (roles.some(role => userRoles.includes(role))) next()
else res.status(401).json({ error: 'Not authorized!' })
} catch (error: any) {
res.status(401).json({ error: error.message })
}
}
}

View File

@ -0,0 +1,20 @@
import { Response, NextFunction } from 'express'
import { RequestWithUser } from '../types.js'
import { Role } from '@prisma/client'
export async function hasRolesMiddleware(roles: Role[]) {
return async function (
req: RequestWithUser,
res: Response,
next: NextFunction
) {
try {
const userRoles = req.user?.roles
if (roles.some(role => userRoles?.includes(role))) next()
else res.status(401).json({ error: 'Not authorized!' })
} catch (error: any) {
res.status(401).json({ error: error.message })
}
}
}