feat: Add API key support
This commit is contained in:
56
src/libs/CookieManager.ts
Normal file
56
src/libs/CookieManager.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import type { ServerResponse } from 'node:http'
|
||||
|
||||
export default class CookieManager {
|
||||
private cookies: Record<string, string> = {}
|
||||
public constructor(data?: string | Record<string, string>) {
|
||||
if (typeof data === 'string') {
|
||||
data.split(';').forEach((keyValuePair) => {
|
||||
const [key, value] = keyValuePair.split('=')
|
||||
this.cookies[key.trim()] = value.trim().replace(/%3B/g, ';')
|
||||
})
|
||||
} else if (typeof data === 'object') {
|
||||
this.cookies = data
|
||||
}
|
||||
}
|
||||
|
||||
public static addCookie(res: ServerResponse, cookie: {
|
||||
key: string
|
||||
value: string
|
||||
expire?: string
|
||||
maxAge?: number
|
||||
domain?: string
|
||||
path?: string
|
||||
secure?: boolean
|
||||
httpOnly?: boolean
|
||||
sameSite?: 'Lax' | 'None' | 'Strict'}
|
||||
) {
|
||||
const items: Array<string> = [`${cookie.key}=${cookie.value.replace(/;/g, '%3B')}`]
|
||||
if (cookie.expire) {
|
||||
items.push(`Expires=${cookie.expire}`)
|
||||
}
|
||||
if (cookie.maxAge) {
|
||||
items.push(`Max-Age=${cookie.maxAge}`)
|
||||
}
|
||||
if (cookie.domain) {
|
||||
items.push(`Domain=${cookie.domain}`)
|
||||
}
|
||||
if (cookie.path) {
|
||||
items.push(`Path=${cookie.path}`)
|
||||
}
|
||||
if (cookie.secure) {
|
||||
items.push('Secure')
|
||||
}
|
||||
if (cookie.httpOnly) {
|
||||
items.push('HttpOnly')
|
||||
}
|
||||
if (cookie.sameSite) {
|
||||
items.push(`SameSite=${cookie.sameSite}`)
|
||||
}
|
||||
res.setHeader('Set-Cookie', items.join('; '))
|
||||
}
|
||||
|
||||
public get(key: string): string | undefined {
|
||||
return this.cookies[key]
|
||||
}
|
||||
|
||||
}
|
@ -52,6 +52,11 @@ export default interface RFC7807 {
|
||||
instance?: string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error the error (base items are type, status, title details and instance)
|
||||
* @returns
|
||||
*/
|
||||
export function buildRFC7807(error: RFC7807 & Record<string, any>): Response {
|
||||
return new Response(JSON.stringify(error), {
|
||||
headers: {
|
||||
|
123
src/libs/validateAuth.ts
Normal file
123
src/libs/validateAuth.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import DaoFactory from '../models/DaoFactory'
|
||||
import CookieManager from './CookieManager'
|
||||
import RFC7807, { buildRFC7807 } from './RFCs/RFC7807'
|
||||
|
||||
interface Permission {
|
||||
name: string
|
||||
/**
|
||||
* if set it will be usable by users
|
||||
*
|
||||
* else only users with the `admin.` prefix in the key can run it
|
||||
*/
|
||||
api: boolean
|
||||
|
||||
/**
|
||||
* if set to true it will pass if a cookie authenticate a valid user
|
||||
*/
|
||||
cookie: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* validate the authentification
|
||||
* @param request the request
|
||||
* @param permission the permission to validate
|
||||
* @returns a Response if the request is invalid, null otherwise
|
||||
*
|
||||
* TODO: implement rate limiting
|
||||
* http/2.0 429 TOO MANY REQUESTS
|
||||
* Content-Type: application/json
|
||||
* X-RateLimit-Limit: 1000
|
||||
* X-RateLimit-Remaining: 0
|
||||
* X-RateLimit-Reset: 123456789
|
||||
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate go back to 0
|
||||
*/
|
||||
export async function validateAuth(request: Request, permission: Permission): Promise<Response | null> {
|
||||
const apiKeyHeader = request.headers.get('Authorization')
|
||||
const cookieHeader = request.headers.get('Cookie')
|
||||
if (apiKeyHeader) {
|
||||
const apiKey = apiKeyHeader.slice(apiKeyHeader.lastIndexOf(' ') + 1)
|
||||
const dao = await DaoFactory.get('apiKey').findOne({
|
||||
key: apiKey
|
||||
})
|
||||
const perm = permission.name.split('.')
|
||||
const match = dao?.permissions.find((it) => {
|
||||
const itSplit = it.split('.')
|
||||
if (itSplit[0] === 'admin') {
|
||||
itSplit.shift()
|
||||
}
|
||||
for (let idx = 0; idx < itSplit.length; idx++) {
|
||||
const permissionLayer = itSplit[idx]
|
||||
const requestPermissionLayer = perm[idx]
|
||||
if (permissionLayer === '*') {
|
||||
return true
|
||||
} else if (permissionLayer !== requestPermissionLayer) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return itSplit.length === perm.length
|
||||
// return it.endsWith(permission.name)
|
||||
})
|
||||
if (match && (permission.api || match.startsWith('admin.'))) {
|
||||
return null
|
||||
} else if (permission.api) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `you are missing the permission "${permission.name}" or is not an admin`
|
||||
})
|
||||
}
|
||||
} else if (permission.api && !permission.cookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `You MUST define an API key fo use this endpoint`
|
||||
})
|
||||
}
|
||||
|
||||
if (cookieHeader && permission.cookie) {
|
||||
// TODO: make a better cookie implementation
|
||||
const cookies = new CookieManager(cookieHeader)
|
||||
const userCookie = cookies.get('userId')
|
||||
if (!userCookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `you must be connected to use this endpoint (missing the userId cookie)`
|
||||
})
|
||||
}
|
||||
const dao = await DaoFactory.get('user').get(userCookie)
|
||||
if (!dao) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `the user does not exists`
|
||||
})
|
||||
}
|
||||
return null
|
||||
} else if (!permission.api && permission.cookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `You MUST be connected to your account to use this endpoint`
|
||||
})
|
||||
} else if (permission.api && permission.cookie) {
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `You must be connected or use an API key to access this endpoint`
|
||||
})
|
||||
}
|
||||
return buildRFC7807({
|
||||
type: '/unauthorized-access',
|
||||
status: 401,
|
||||
title: 'Unauthorized access',
|
||||
details: `the following endpoint is not currently accessible by any means`
|
||||
})
|
||||
|
||||
}
|
Reference in New Issue
Block a user