Refactoring
This commit is contained in:
parent
26771cecdc
commit
c8557b9442
|
@ -1,12 +1,14 @@
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { prisma } from './prisma.js'
|
import { prisma } from './prisma.js'
|
||||||
import { ldapClient } from './ldapClient.js'
|
import { LdapController } from './controllers/LdapController.js'
|
||||||
import { UserController } from './controllers/UserController.js'
|
import { UserController } from './controllers/UserController.js'
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret'
|
const JWT_SECRET = process.env.JWT_SECRET || 'secret'
|
||||||
|
|
||||||
export async function login(username: string, password: string) {
|
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)
|
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'
|
import { prisma } from '../prisma.js'
|
||||||
|
|
||||||
export class UserController {
|
export class UserController {
|
||||||
static async importUser(username: string) {
|
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({
|
return await prisma.user.upsert({
|
||||||
where: { username: user.username },
|
where: { username: user.username },
|
||||||
|
@ -11,19 +15,4 @@ export class UserController {
|
||||||
create: user
|
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 express, { Request, Response } from 'express'
|
||||||
import bodyParser from 'body-parser'
|
import bodyParser from 'body-parser'
|
||||||
|
|
||||||
import {
|
import { injectUserMiddleware } from './middleware/injectUserMiddleware.js'
|
||||||
authenticatedMiddleware,
|
import { authMiddleware } from './middleware/authMiddleware.js'
|
||||||
hasRolesMiddleware
|
import { hasRolesMiddleware } from './middleware/hasRolesMiddleware.js'
|
||||||
} from './middleware/authorization.js'
|
|
||||||
import { injectUserMiddleware } from './middleware/injectUser.js'
|
|
||||||
|
|
||||||
import { RequestWithUser } from './types.js'
|
import { RequestWithUser } from './types.js'
|
||||||
import { login } from './authentication.js'
|
import { login } from './authentication.js'
|
||||||
import { UserController } from './controllers/UserController.js'
|
|
||||||
|
import { UserRouteController } from './controllers/UserRouteController.js'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
@ -44,15 +43,15 @@ app.post('/api/login', async (req: Request, res: Response) => {
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/api/me',
|
'/api/me',
|
||||||
authenticatedMiddleware,
|
authMiddleware,
|
||||||
async (req: RequestWithUser, res: Response) => res.json(req.user)
|
async (req: RequestWithUser, res: Response) => res.json(req.user)
|
||||||
)
|
)
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/api/protected',
|
'/api/protected',
|
||||||
authenticatedMiddleware,
|
authMiddleware,
|
||||||
async (req: RequestWithUser, res: Response) => {
|
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',
|
'/api/admin',
|
||||||
await hasRolesMiddleware(['ADMIN']),
|
await hasRolesMiddleware(['ADMIN']),
|
||||||
async (req: RequestWithUser, res: Response) => {
|
async (req: RequestWithUser, res: Response) => {
|
||||||
res.send('Hello Admin!' + req.user?.username)
|
res.json('Hello Admin!' + req.user?.username)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/api/user/:username',
|
'/api/user/:username',
|
||||||
await hasRolesMiddleware(['ADMIN']),
|
await hasRolesMiddleware(['ADMIN']),
|
||||||
async (req: Request, res: Response) => {
|
UserRouteController.get
|
||||||
const { username } = req.params
|
|
||||||
|
|
||||||
res.json(await UserController.getUser(username))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start server
|
// 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