Refactoring
This commit is contained in:
parent
26771cecdc
commit
c8557b9442
|
@ -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)
|
||||
|
||||
|
|
1
src/controllers/AuthenticationController.ts
Normal file
1
src/controllers/AuthenticationController.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export class LoginController {}
|
85
src/controllers/LdapController.ts
Normal file
85
src/controllers/LdapController.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
24
src/controllers/UserRouteController.ts
Normal file
24
src/controllers/UserRouteController.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
}
|
25
src/index.ts
25
src/index.ts
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
19
src/middleware/authMiddleware.ts
Normal file
19
src/middleware/authMiddleware.ts
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
20
src/middleware/hasRolesMiddleware.ts
Normal file
20
src/middleware/hasRolesMiddleware.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user