@ahmedbaset/adminjs-hono 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +20 -0
- package/README.md +239 -0
- package/examples/auth.ts +76 -0
- package/examples/simple.ts +42 -0
- package/lib/authentication/login.handler.d.ts +11 -0
- package/lib/authentication/login.handler.d.ts.map +1 -0
- package/lib/authentication/login.handler.js +155 -0
- package/lib/authentication/logout.handler.d.ts +11 -0
- package/lib/authentication/logout.handler.d.ts.map +1 -0
- package/lib/authentication/logout.handler.js +50 -0
- package/lib/authentication/protected-routes.handler.d.ts +11 -0
- package/lib/authentication/protected-routes.handler.d.ts.map +1 -0
- package/lib/authentication/protected-routes.handler.js +26 -0
- package/lib/authentication/refresh.handler.d.ts +13 -0
- package/lib/authentication/refresh.handler.d.ts.map +1 -0
- package/lib/authentication/refresh.handler.js +42 -0
- package/lib/buildAuthenticatedRouter.d.ts +15 -0
- package/lib/buildAuthenticatedRouter.d.ts.map +1 -0
- package/lib/buildAuthenticatedRouter.js +61 -0
- package/lib/buildRouter.d.ts +53 -0
- package/lib/buildRouter.d.ts.map +1 -0
- package/lib/buildRouter.js +178 -0
- package/lib/convertRoutes.d.ts +9 -0
- package/lib/convertRoutes.d.ts.map +1 -0
- package/lib/convertRoutes.js +10 -0
- package/lib/errors.d.ts +10 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +15 -0
- package/lib/formParser.d.ts +13 -0
- package/lib/formParser.d.ts.map +1 -0
- package/lib/formParser.js +53 -0
- package/lib/index.d.ts +55 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +48 -0
- package/lib/logger.d.ts +7 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +17 -0
- package/lib/session.d.ts +25 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/session.js +56 -0
- package/lib/types.d.ts +46 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +1 -0
- package/package.json +44 -0
- package/src/authentication/login.handler.ts +193 -0
- package/src/authentication/logout.handler.ts +62 -0
- package/src/authentication/protected-routes.handler.ts +38 -0
- package/src/authentication/refresh.handler.ts +59 -0
- package/src/buildAuthenticatedRouter.ts +92 -0
- package/src/buildRouter.ts +224 -0
- package/src/convertRoutes.ts +10 -0
- package/src/errors.ts +24 -0
- package/src/formParser.ts +73 -0
- package/src/index.ts +74 -0
- package/src/logger.ts +18 -0
- package/src/session.ts +71 -0
- package/src/types.ts +53 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import AdminJS, { Router as AdminRouter } from 'adminjs'
|
|
2
|
+
import type { Hono, Handler, Context } from 'hono'
|
|
3
|
+
import { WrongArgumentError, INVALID_ADMINJS_INSTANCE } from './errors.js'
|
|
4
|
+
import { log } from './logger.js'
|
|
5
|
+
import { convertToHonoRoute } from './convertRoutes.js'
|
|
6
|
+
import type { HonoVariables } from './types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates and initializes an AdminJS instance
|
|
10
|
+
* @param admin - The AdminJS instance to initialize
|
|
11
|
+
* @throws {WrongArgumentError} If the admin parameter is not a valid AdminJS instance
|
|
12
|
+
*/
|
|
13
|
+
export function initializeAdmin(admin: AdminJS): void {
|
|
14
|
+
if (admin?.constructor?.name !== 'AdminJS') {
|
|
15
|
+
throw new WrongArgumentError(INVALID_ADMINJS_INSTANCE)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
admin.initialize().then(() => {
|
|
19
|
+
log.debug('AdminJS: bundle ready')
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a Hono route handler from an AdminJS route
|
|
25
|
+
* Extracts request data and invokes the AdminJS controller
|
|
26
|
+
*
|
|
27
|
+
* @param admin - The AdminJS instance
|
|
28
|
+
* @param route - The AdminJS route definition
|
|
29
|
+
* @returns Hono handler function
|
|
30
|
+
*/
|
|
31
|
+
export function routeHandler(
|
|
32
|
+
admin: AdminJS,
|
|
33
|
+
route: (typeof AdminRouter)['routes'][0]
|
|
34
|
+
): Handler<{ Variables: HonoVariables }> {
|
|
35
|
+
return async (c: Context<{ Variables: HonoVariables }>) => {
|
|
36
|
+
// Get session from context (may be undefined for non-authenticated routes)
|
|
37
|
+
const session = c.get('session')
|
|
38
|
+
const adminUser = session?.adminUser
|
|
39
|
+
|
|
40
|
+
// Instantiate the AdminJS controller
|
|
41
|
+
const controller = new route.Controller({ admin }, adminUser)
|
|
42
|
+
|
|
43
|
+
// Extract request data
|
|
44
|
+
const params = c.req.param()
|
|
45
|
+
const query = c.req.query()
|
|
46
|
+
const method = c.req.method.toLowerCase()
|
|
47
|
+
|
|
48
|
+
// Get parsed form data from context (set by form parser middleware)
|
|
49
|
+
const fields = c.get('fields') || {}
|
|
50
|
+
const files = c.get('files') || {}
|
|
51
|
+
const payload = {
|
|
52
|
+
...fields,
|
|
53
|
+
...files,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Execute the controller action
|
|
57
|
+
const html = await controller[route.action](
|
|
58
|
+
{
|
|
59
|
+
...c.req,
|
|
60
|
+
params,
|
|
61
|
+
query,
|
|
62
|
+
payload,
|
|
63
|
+
method,
|
|
64
|
+
},
|
|
65
|
+
c.res
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Set Content-Type header if specified
|
|
69
|
+
if (route.contentType) {
|
|
70
|
+
c.header('Content-Type', route.contentType)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Return response
|
|
74
|
+
if (html) {
|
|
75
|
+
return c.body(html)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If no response body, return empty response
|
|
79
|
+
return c.body(null)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Registers a single AdminJS route with the Hono app
|
|
85
|
+
* @param route - The AdminJS route to register
|
|
86
|
+
* @param app - The Hono app instance
|
|
87
|
+
* @param admin - The AdminJS instance
|
|
88
|
+
*/
|
|
89
|
+
export function buildRoute(
|
|
90
|
+
route: (typeof AdminRouter)['routes'][number],
|
|
91
|
+
app: Hono,
|
|
92
|
+
admin: AdminJS
|
|
93
|
+
): void {
|
|
94
|
+
// Convert AdminJS route path to Hono format
|
|
95
|
+
const honoPath = convertToHonoRoute(route.path)
|
|
96
|
+
|
|
97
|
+
// Register handler based on HTTP method
|
|
98
|
+
if (route.method === 'GET') {
|
|
99
|
+
app.get(honoPath, routeHandler(admin, route))
|
|
100
|
+
} else if (route.method === 'POST') {
|
|
101
|
+
app.post(honoPath, routeHandler(admin, route))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Registers all AdminJS routes with the Hono app
|
|
107
|
+
* @param routes - Array of AdminJS routes
|
|
108
|
+
* @param app - The Hono app instance
|
|
109
|
+
* @param admin - The AdminJS instance
|
|
110
|
+
*/
|
|
111
|
+
export function buildRoutes(
|
|
112
|
+
routes: (typeof AdminRouter)['routes'],
|
|
113
|
+
app: Hono,
|
|
114
|
+
admin: AdminJS
|
|
115
|
+
): void {
|
|
116
|
+
routes.forEach((route) => buildRoute(route, app, admin))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Determines Content-Type header based on file extension
|
|
121
|
+
* @param path - File path
|
|
122
|
+
* @returns Content-Type string
|
|
123
|
+
*/
|
|
124
|
+
function getContentType(path: string): string {
|
|
125
|
+
if (path.endsWith('.js')) {
|
|
126
|
+
return 'application/javascript'
|
|
127
|
+
} else if (path.endsWith('.css')) {
|
|
128
|
+
return 'text/css'
|
|
129
|
+
} else if (path.endsWith('.html')) {
|
|
130
|
+
return 'text/html'
|
|
131
|
+
} else if (path.endsWith('.json')) {
|
|
132
|
+
return 'application/json'
|
|
133
|
+
} else if (path.endsWith('.png')) {
|
|
134
|
+
return 'image/png'
|
|
135
|
+
} else if (path.endsWith('.jpg') || path.endsWith('.jpeg')) {
|
|
136
|
+
return 'image/jpeg'
|
|
137
|
+
} else if (path.endsWith('.svg')) {
|
|
138
|
+
return 'image/svg+xml'
|
|
139
|
+
}
|
|
140
|
+
return 'application/octet-stream'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Registers AdminJS asset routes with the Hono app
|
|
145
|
+
* @param assets - Array of AdminJS assets
|
|
146
|
+
* @param routes - Array of AdminJS routes (to find component bundler route)
|
|
147
|
+
* @param app - The Hono app instance
|
|
148
|
+
* @param admin - The AdminJS instance
|
|
149
|
+
*/
|
|
150
|
+
export function buildAssets(
|
|
151
|
+
assets: (typeof AdminRouter)['assets'],
|
|
152
|
+
routes: (typeof AdminRouter)['routes'],
|
|
153
|
+
app: Hono,
|
|
154
|
+
admin: AdminJS
|
|
155
|
+
): void {
|
|
156
|
+
// Register component bundler route if it exists
|
|
157
|
+
const componentBundlerRoute = routes.find(
|
|
158
|
+
(r) => r.action === 'bundleComponents'
|
|
159
|
+
)
|
|
160
|
+
if (componentBundlerRoute) {
|
|
161
|
+
buildRoute(componentBundlerRoute, app, admin)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Register static asset routes
|
|
165
|
+
assets.forEach((asset) => {
|
|
166
|
+
app.get(asset.path, async (c) => {
|
|
167
|
+
try {
|
|
168
|
+
// Read file using Node.js fs (for Node runtime)
|
|
169
|
+
// For other runtimes, this would need runtime detection
|
|
170
|
+
const fs = await import('fs/promises')
|
|
171
|
+
const path = await import('path')
|
|
172
|
+
|
|
173
|
+
const filePath = path.resolve(asset.src)
|
|
174
|
+
const fileContent = await fs.readFile(filePath)
|
|
175
|
+
|
|
176
|
+
// Set appropriate Content-Type header
|
|
177
|
+
const contentType = getContentType(asset.path)
|
|
178
|
+
c.header('Content-Type', contentType)
|
|
179
|
+
|
|
180
|
+
return c.body(fileContent)
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`Error serving asset ${asset.path}:`, error)
|
|
183
|
+
return c.text('Asset not found', 404)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
import type { UploadOptions } from './types.js'
|
|
190
|
+
import { createFormParserMiddleware } from './formParser.js'
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Builds a Hono app with AdminJS routes (without authentication)
|
|
194
|
+
*
|
|
195
|
+
* @param admin - The AdminJS instance
|
|
196
|
+
* @param predefinedApp - Optional existing Hono app to use
|
|
197
|
+
* @param uploadOptions - Optional upload configuration
|
|
198
|
+
* @returns Configured Hono app
|
|
199
|
+
*/
|
|
200
|
+
export function buildRouter(
|
|
201
|
+
admin: AdminJS,
|
|
202
|
+
predefinedApp?: Hono,
|
|
203
|
+
uploadOptions?: UploadOptions
|
|
204
|
+
): Hono {
|
|
205
|
+
// Initialize AdminJS
|
|
206
|
+
initializeAdmin(admin)
|
|
207
|
+
|
|
208
|
+
// Use provided app or create new Hono instance
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
210
|
+
const { Hono: HonoClass } = require('hono')
|
|
211
|
+
const app = predefinedApp ?? new HonoClass()
|
|
212
|
+
|
|
213
|
+
// Get routes and assets from AdminJS
|
|
214
|
+
const { routes, assets } = AdminRouter
|
|
215
|
+
|
|
216
|
+
// Register form parsing middleware
|
|
217
|
+
app.use('*', createFormParserMiddleware(uploadOptions))
|
|
218
|
+
|
|
219
|
+
// Build assets and routes
|
|
220
|
+
buildAssets(assets, routes, app, admin)
|
|
221
|
+
buildRoutes(routes, app, admin)
|
|
222
|
+
|
|
223
|
+
return app
|
|
224
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts AdminJS route path format to Hono route path format
|
|
3
|
+
* AdminJS uses {param} notation while Hono uses :param notation
|
|
4
|
+
*
|
|
5
|
+
* @param path - AdminJS route path (e.g., "/resources/{resourceId}/actions/{action}")
|
|
6
|
+
* @returns Hono route path (e.g., "/resources/:resourceId/actions/:action")
|
|
7
|
+
*/
|
|
8
|
+
export function convertToHonoRoute(path: string): string {
|
|
9
|
+
return path.replace(/\{([^}]+)\}/g, ':$1')
|
|
10
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class WrongArgumentError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message)
|
|
4
|
+
this.name = 'WrongArgumentError'
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const INVALID_ADMINJS_INSTANCE =
|
|
9
|
+
'You have to pass an instance of AdminJS to the buildRouter() function'
|
|
10
|
+
|
|
11
|
+
export const MISSING_AUTH_CONFIG_ERROR =
|
|
12
|
+
'You must provide either "authenticate" function or "provider" in authentication options'
|
|
13
|
+
|
|
14
|
+
export const INVALID_AUTH_CONFIG_ERROR =
|
|
15
|
+
'You cannot provide both "authenticate" function and "provider" in authentication options'
|
|
16
|
+
|
|
17
|
+
export class OldBodyParserUsedError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super(
|
|
20
|
+
'You are using old body-parser middleware which is not compatible with AdminJS. Please remove it.'
|
|
21
|
+
)
|
|
22
|
+
this.name = 'OldBodyParserUsedError'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MiddlewareHandler, Context } from 'hono'
|
|
2
|
+
import type { UploadOptions, HonoVariables } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates middleware to parse form data (multipart/form-data and application/x-www-form-urlencoded)
|
|
6
|
+
* Extracts fields and files from the request and stores them in context variables
|
|
7
|
+
*
|
|
8
|
+
* @param options - Upload configuration options
|
|
9
|
+
* @returns Hono middleware handler
|
|
10
|
+
*/
|
|
11
|
+
export function createFormParserMiddleware(
|
|
12
|
+
options?: UploadOptions
|
|
13
|
+
): MiddlewareHandler<{ Variables: HonoVariables }> {
|
|
14
|
+
return async (c: Context<{ Variables: HonoVariables }>, next) => {
|
|
15
|
+
const contentType = c.req.header('content-type')
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
contentType?.includes('multipart/form-data') ||
|
|
19
|
+
contentType?.includes('application/x-www-form-urlencoded')
|
|
20
|
+
) {
|
|
21
|
+
try {
|
|
22
|
+
const formData = await c.req.formData()
|
|
23
|
+
const fields: Record<string, any> = {}
|
|
24
|
+
const files: Record<string, File> = {}
|
|
25
|
+
|
|
26
|
+
for (const [key, value] of formData.entries()) {
|
|
27
|
+
if (value instanceof File) {
|
|
28
|
+
// Check file size if maxFileSize is specified
|
|
29
|
+
if (options?.maxFileSize && value.size > options.maxFileSize) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`File ${value.name} exceeds maximum size of ${options.maxFileSize} bytes`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
files[key] = value
|
|
35
|
+
} else {
|
|
36
|
+
fields[key] = value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check total fields size if maxFieldsSize is specified
|
|
41
|
+
if (options?.maxFieldsSize) {
|
|
42
|
+
const totalFieldsSize = Object.values(fields).reduce(
|
|
43
|
+
(sum, val) => sum + String(val).length,
|
|
44
|
+
0
|
|
45
|
+
)
|
|
46
|
+
if (totalFieldsSize > options.maxFieldsSize) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Total fields size exceeds maximum of ${options.maxFieldsSize} bytes`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check number of fields if maxFields is specified
|
|
54
|
+
if (options?.maxFields && Object.keys(fields).length > options.maxFields) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Number of fields exceeds maximum of ${options.maxFields}`
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
c.set('fields', fields)
|
|
61
|
+
c.set('files', files)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Handle parsing errors gracefully
|
|
64
|
+
console.error('Form parsing error:', error)
|
|
65
|
+
c.set('fields', {})
|
|
66
|
+
c.set('files', {})
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await next()
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildAuthenticatedRouter } from './buildAuthenticatedRouter.js'
|
|
2
|
+
import { buildRouter } from './buildRouter.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module @ahmedbaset/adminjs-hono
|
|
6
|
+
*
|
|
7
|
+
* AdminJS adapter for Hono web framework
|
|
8
|
+
*
|
|
9
|
+
* Provides two main functions:
|
|
10
|
+
* - buildRouter: Creates a Hono app with AdminJS routes (no authentication)
|
|
11
|
+
* - buildAuthenticatedRouter: Creates a Hono app with AdminJS routes protected by session authentication
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { Hono } from 'hono'
|
|
16
|
+
* import AdminJS from 'adminjs'
|
|
17
|
+
* import { buildRouter } from '@ahmedbaset/adminjs-hono'
|
|
18
|
+
*
|
|
19
|
+
* const admin = new AdminJS({
|
|
20
|
+
* databases: [],
|
|
21
|
+
* rootPath: '/admin',
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* const app = new Hono()
|
|
25
|
+
* const adminRouter = buildRouter(admin)
|
|
26
|
+
* app.route('/admin', adminRouter)
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Plugin name
|
|
32
|
+
*/
|
|
33
|
+
export const name = 'AdminJSHono'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Plugin interface
|
|
37
|
+
*/
|
|
38
|
+
export type HonoPlugin = {
|
|
39
|
+
name: string
|
|
40
|
+
buildAuthenticatedRouter: typeof buildAuthenticatedRouter
|
|
41
|
+
buildRouter: typeof buildRouter
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default plugin export
|
|
46
|
+
*/
|
|
47
|
+
const plugin: HonoPlugin = { name, buildAuthenticatedRouter, buildRouter }
|
|
48
|
+
|
|
49
|
+
export default plugin
|
|
50
|
+
|
|
51
|
+
// Export main functions
|
|
52
|
+
export { buildRouter } from './buildRouter.js'
|
|
53
|
+
export { buildAuthenticatedRouter } from './buildAuthenticatedRouter.js'
|
|
54
|
+
|
|
55
|
+
// Export types
|
|
56
|
+
export type {
|
|
57
|
+
AuthenticationOptions,
|
|
58
|
+
UploadOptions,
|
|
59
|
+
AuthenticationMaxRetriesOptions,
|
|
60
|
+
AuthenticationContext,
|
|
61
|
+
SessionOptions,
|
|
62
|
+
SessionData,
|
|
63
|
+
} from './types.js'
|
|
64
|
+
|
|
65
|
+
// Export utilities
|
|
66
|
+
export { convertToHonoRoute } from './convertRoutes.js'
|
|
67
|
+
export * from './errors.js'
|
|
68
|
+
export { log } from './logger.js'
|
|
69
|
+
|
|
70
|
+
// Export authentication handlers (for advanced usage)
|
|
71
|
+
export { withLogin } from './authentication/login.handler.js'
|
|
72
|
+
export { withLogout } from './authentication/logout.handler.js'
|
|
73
|
+
export { withProtectedRoutesHandler } from './authentication/protected-routes.handler.js'
|
|
74
|
+
export { withRefresh } from './authentication/refresh.handler.js'
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const DEBUG = process.env.ADMINJS_HONO_DEBUG === 'true'
|
|
2
|
+
|
|
3
|
+
export const log = {
|
|
4
|
+
debug: (...args: unknown[]) => {
|
|
5
|
+
if (DEBUG) {
|
|
6
|
+
console.log('[AdminJS Hono]', ...args)
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
info: (...args: unknown[]) => {
|
|
10
|
+
console.log('[AdminJS Hono]', ...args)
|
|
11
|
+
},
|
|
12
|
+
warn: (...args: unknown[]) => {
|
|
13
|
+
console.warn('[AdminJS Hono]', ...args)
|
|
14
|
+
},
|
|
15
|
+
error: (...args: unknown[]) => {
|
|
16
|
+
console.error('[AdminJS Hono]', ...args)
|
|
17
|
+
},
|
|
18
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getCookie, setCookie } from 'hono/cookie'
|
|
2
|
+
import type { MiddlewareHandler, Context } from 'hono'
|
|
3
|
+
import type { SessionData, SessionOptions, HonoVariables } from './types.js'
|
|
4
|
+
|
|
5
|
+
// In-memory session store (not suitable for production with multiple instances)
|
|
6
|
+
const sessions = new Map<string, SessionData>()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a unique session ID using Web Crypto API
|
|
10
|
+
* @returns A unique session identifier
|
|
11
|
+
*/
|
|
12
|
+
function generateSessionId(): string {
|
|
13
|
+
return crypto.randomUUID()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates session middleware for Hono
|
|
18
|
+
* Manages cookie-based sessions with in-memory storage
|
|
19
|
+
*
|
|
20
|
+
* @param secret - Secret key for session (currently unused, for future HMAC signing)
|
|
21
|
+
* @param cookieName - Name of the session cookie
|
|
22
|
+
* @param options - Session cookie options
|
|
23
|
+
* @returns Hono middleware handler
|
|
24
|
+
*/
|
|
25
|
+
export function createSessionMiddleware(
|
|
26
|
+
secret: string,
|
|
27
|
+
cookieName: string,
|
|
28
|
+
options?: SessionOptions
|
|
29
|
+
): MiddlewareHandler<{ Variables: HonoVariables }> {
|
|
30
|
+
return async (c: Context<{ Variables: HonoVariables }>, next) => {
|
|
31
|
+
let sessionId = getCookie(c, cookieName)
|
|
32
|
+
|
|
33
|
+
// Create new session if none exists or session not found
|
|
34
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
35
|
+
sessionId = generateSessionId()
|
|
36
|
+
sessions.set(sessionId, {})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get session data and store in context
|
|
40
|
+
const session = sessions.get(sessionId)!
|
|
41
|
+
c.set('session', session)
|
|
42
|
+
|
|
43
|
+
await next()
|
|
44
|
+
|
|
45
|
+
// Save session cookie after request processing
|
|
46
|
+
setCookie(c, cookieName, sessionId, {
|
|
47
|
+
httpOnly: options?.httpOnly ?? true,
|
|
48
|
+
secure: options?.secure ?? false,
|
|
49
|
+
sameSite: options?.sameSite ?? 'Lax',
|
|
50
|
+
maxAge: options?.maxAge ?? 86400, // 24 hours default
|
|
51
|
+
path: options?.path ?? '/',
|
|
52
|
+
domain: options?.domain,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Destroys a session by removing it from the store
|
|
59
|
+
* @param sessionId - The session ID to destroy
|
|
60
|
+
*/
|
|
61
|
+
export function destroySession(sessionId: string): void {
|
|
62
|
+
sessions.delete(sessionId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gets the session store (for testing purposes)
|
|
67
|
+
* @returns The session store Map
|
|
68
|
+
*/
|
|
69
|
+
export function getSessionStore(): Map<string, SessionData> {
|
|
70
|
+
return sessions
|
|
71
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BaseAuthProvider, CurrentAdmin } from 'adminjs'
|
|
2
|
+
import type { Context } from 'hono'
|
|
3
|
+
|
|
4
|
+
export type UploadOptions = {
|
|
5
|
+
uploadDir?: string
|
|
6
|
+
maxFileSize?: number
|
|
7
|
+
maxFieldsSize?: number
|
|
8
|
+
maxFields?: number
|
|
9
|
+
keepExtensions?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Hono context variables
|
|
13
|
+
export type HonoVariables = {
|
|
14
|
+
session: SessionData
|
|
15
|
+
fields: Record<string, any>
|
|
16
|
+
files: Record<string, File>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AuthenticationContext = {
|
|
20
|
+
req: Context<{ Variables: HonoVariables }>
|
|
21
|
+
res: Context<{ Variables: HonoVariables }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AuthenticationMaxRetriesOptions = {
|
|
25
|
+
count: number
|
|
26
|
+
duration: number // in seconds
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AuthenticationOptions = {
|
|
30
|
+
cookiePassword: string
|
|
31
|
+
cookieName?: string
|
|
32
|
+
authenticate?: (
|
|
33
|
+
email: string,
|
|
34
|
+
password: string,
|
|
35
|
+
context?: AuthenticationContext
|
|
36
|
+
) => Promise<CurrentAdmin | null> | CurrentAdmin | null
|
|
37
|
+
maxRetries?: number | AuthenticationMaxRetriesOptions
|
|
38
|
+
provider?: BaseAuthProvider
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SessionOptions = {
|
|
42
|
+
maxAge?: number
|
|
43
|
+
httpOnly?: boolean
|
|
44
|
+
secure?: boolean
|
|
45
|
+
sameSite?: 'Strict' | 'Lax' | 'None'
|
|
46
|
+
domain?: string
|
|
47
|
+
path?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type SessionData = {
|
|
51
|
+
adminUser?: CurrentAdmin
|
|
52
|
+
redirectTo?: string
|
|
53
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"outDir": "./lib",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"types": ["node"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "lib", "test", "examples"]
|
|
21
|
+
}
|