@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.
Files changed (59) hide show
  1. package/.eslintrc.cjs +20 -0
  2. package/README.md +239 -0
  3. package/examples/auth.ts +76 -0
  4. package/examples/simple.ts +42 -0
  5. package/lib/authentication/login.handler.d.ts +11 -0
  6. package/lib/authentication/login.handler.d.ts.map +1 -0
  7. package/lib/authentication/login.handler.js +155 -0
  8. package/lib/authentication/logout.handler.d.ts +11 -0
  9. package/lib/authentication/logout.handler.d.ts.map +1 -0
  10. package/lib/authentication/logout.handler.js +50 -0
  11. package/lib/authentication/protected-routes.handler.d.ts +11 -0
  12. package/lib/authentication/protected-routes.handler.d.ts.map +1 -0
  13. package/lib/authentication/protected-routes.handler.js +26 -0
  14. package/lib/authentication/refresh.handler.d.ts +13 -0
  15. package/lib/authentication/refresh.handler.d.ts.map +1 -0
  16. package/lib/authentication/refresh.handler.js +42 -0
  17. package/lib/buildAuthenticatedRouter.d.ts +15 -0
  18. package/lib/buildAuthenticatedRouter.d.ts.map +1 -0
  19. package/lib/buildAuthenticatedRouter.js +61 -0
  20. package/lib/buildRouter.d.ts +53 -0
  21. package/lib/buildRouter.d.ts.map +1 -0
  22. package/lib/buildRouter.js +178 -0
  23. package/lib/convertRoutes.d.ts +9 -0
  24. package/lib/convertRoutes.d.ts.map +1 -0
  25. package/lib/convertRoutes.js +10 -0
  26. package/lib/errors.d.ts +10 -0
  27. package/lib/errors.d.ts.map +1 -0
  28. package/lib/errors.js +15 -0
  29. package/lib/formParser.d.ts +13 -0
  30. package/lib/formParser.d.ts.map +1 -0
  31. package/lib/formParser.js +53 -0
  32. package/lib/index.d.ts +55 -0
  33. package/lib/index.d.ts.map +1 -0
  34. package/lib/index.js +48 -0
  35. package/lib/logger.d.ts +7 -0
  36. package/lib/logger.d.ts.map +1 -0
  37. package/lib/logger.js +17 -0
  38. package/lib/session.d.ts +25 -0
  39. package/lib/session.d.ts.map +1 -0
  40. package/lib/session.js +56 -0
  41. package/lib/types.d.ts +46 -0
  42. package/lib/types.d.ts.map +1 -0
  43. package/lib/types.js +1 -0
  44. package/package.json +44 -0
  45. package/src/authentication/login.handler.ts +193 -0
  46. package/src/authentication/logout.handler.ts +62 -0
  47. package/src/authentication/protected-routes.handler.ts +38 -0
  48. package/src/authentication/refresh.handler.ts +59 -0
  49. package/src/buildAuthenticatedRouter.ts +92 -0
  50. package/src/buildRouter.ts +224 -0
  51. package/src/convertRoutes.ts +10 -0
  52. package/src/errors.ts +24 -0
  53. package/src/formParser.ts +73 -0
  54. package/src/index.ts +74 -0
  55. package/src/logger.ts +18 -0
  56. package/src/session.ts +71 -0
  57. package/src/types.ts +53 -0
  58. package/tsconfig.json +21 -0
  59. package/vitest.config.ts +12 -0
package/lib/session.js ADDED
@@ -0,0 +1,56 @@
1
+ import { getCookie, setCookie } from 'hono/cookie';
2
+ // In-memory session store (not suitable for production with multiple instances)
3
+ const sessions = new Map();
4
+ /**
5
+ * Generates a unique session ID using Web Crypto API
6
+ * @returns A unique session identifier
7
+ */
8
+ function generateSessionId() {
9
+ return crypto.randomUUID();
10
+ }
11
+ /**
12
+ * Creates session middleware for Hono
13
+ * Manages cookie-based sessions with in-memory storage
14
+ *
15
+ * @param secret - Secret key for session (currently unused, for future HMAC signing)
16
+ * @param cookieName - Name of the session cookie
17
+ * @param options - Session cookie options
18
+ * @returns Hono middleware handler
19
+ */
20
+ export function createSessionMiddleware(secret, cookieName, options) {
21
+ return async (c, next) => {
22
+ let sessionId = getCookie(c, cookieName);
23
+ // Create new session if none exists or session not found
24
+ if (!sessionId || !sessions.has(sessionId)) {
25
+ sessionId = generateSessionId();
26
+ sessions.set(sessionId, {});
27
+ }
28
+ // Get session data and store in context
29
+ const session = sessions.get(sessionId);
30
+ c.set('session', session);
31
+ await next();
32
+ // Save session cookie after request processing
33
+ setCookie(c, cookieName, sessionId, {
34
+ httpOnly: options?.httpOnly ?? true,
35
+ secure: options?.secure ?? false,
36
+ sameSite: options?.sameSite ?? 'Lax',
37
+ maxAge: options?.maxAge ?? 86400, // 24 hours default
38
+ path: options?.path ?? '/',
39
+ domain: options?.domain,
40
+ });
41
+ };
42
+ }
43
+ /**
44
+ * Destroys a session by removing it from the store
45
+ * @param sessionId - The session ID to destroy
46
+ */
47
+ export function destroySession(sessionId) {
48
+ sessions.delete(sessionId);
49
+ }
50
+ /**
51
+ * Gets the session store (for testing purposes)
52
+ * @returns The session store Map
53
+ */
54
+ export function getSessionStore() {
55
+ return sessions;
56
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { BaseAuthProvider, CurrentAdmin } from 'adminjs';
2
+ import type { Context } from 'hono';
3
+ export type UploadOptions = {
4
+ uploadDir?: string;
5
+ maxFileSize?: number;
6
+ maxFieldsSize?: number;
7
+ maxFields?: number;
8
+ keepExtensions?: boolean;
9
+ };
10
+ export type HonoVariables = {
11
+ session: SessionData;
12
+ fields: Record<string, any>;
13
+ files: Record<string, File>;
14
+ };
15
+ export type AuthenticationContext = {
16
+ req: Context<{
17
+ Variables: HonoVariables;
18
+ }>;
19
+ res: Context<{
20
+ Variables: HonoVariables;
21
+ }>;
22
+ };
23
+ export type AuthenticationMaxRetriesOptions = {
24
+ count: number;
25
+ duration: number;
26
+ };
27
+ export type AuthenticationOptions = {
28
+ cookiePassword: string;
29
+ cookieName?: string;
30
+ authenticate?: (email: string, password: string, context?: AuthenticationContext) => Promise<CurrentAdmin | null> | CurrentAdmin | null;
31
+ maxRetries?: number | AuthenticationMaxRetriesOptions;
32
+ provider?: BaseAuthProvider;
33
+ };
34
+ export type SessionOptions = {
35
+ maxAge?: number;
36
+ httpOnly?: boolean;
37
+ secure?: boolean;
38
+ sameSite?: 'Strict' | 'Lax' | 'None';
39
+ domain?: string;
40
+ path?: string;
41
+ };
42
+ export type SessionData = {
43
+ adminUser?: CurrentAdmin;
44
+ redirectTo?: string;
45
+ };
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAGD,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,OAAO,CAAC;QAAE,SAAS,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;IAC1C,GAAG,EAAE,OAAO,CAAC;QAAE,SAAS,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;CAC3C,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,CACb,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,YAAY,GAAG,IAAI,CAAA;IACvD,UAAU,CAAC,EAAE,MAAM,GAAG,+BAA+B,CAAA;IACrD,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA"}
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@ahmedbaset/adminjs-hono",
3
+ "version": "0.1.0",
4
+ "description": "AdminJS adapter for Hono web framework",
5
+ "main": "lib/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./lib/index.js",
10
+ "types": "./lib/index.d.ts"
11
+ }
12
+ },
13
+ "keywords": [
14
+ "hono",
15
+ "admin",
16
+ "adminjs",
17
+ "admin-panel"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "peerDependencies": {
22
+ "adminjs": "^7.4.0",
23
+ "hono": "^4.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.0.0",
27
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
28
+ "@typescript-eslint/parser": "^6.0.0",
29
+ "adminjs": "^7.4.0",
30
+ "eslint": "^8.0.0",
31
+ "fast-check": "^3.15.0",
32
+ "hono": "^4.0.0",
33
+ "typescript": "^5.3.0",
34
+ "vitest": "^1.2.0"
35
+ },
36
+ "scripts": {
37
+ "dev": "rm -rf lib && tsc --watch",
38
+ "build": "rm -rf lib && tsc",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "lint": "eslint './**/*.ts'",
42
+ "check:all": "npm run lint && npm run build && npm run test"
43
+ }
44
+ }
@@ -0,0 +1,193 @@
1
+ import type AdminJS from 'adminjs'
2
+ import type { Hono, Context } from 'hono'
3
+ import type {
4
+ AuthenticationContext,
5
+ AuthenticationMaxRetriesOptions,
6
+ AuthenticationOptions,
7
+ HonoVariables,
8
+ } from '../types.js'
9
+ import { INVALID_AUTH_CONFIG_ERROR, WrongArgumentError } from '../errors.js'
10
+
11
+ /**
12
+ * Normalizes the login path by removing the root path
13
+ * @param admin - AdminJS instance
14
+ * @returns Normalized login path
15
+ */
16
+ function getLoginPath(admin: AdminJS): string {
17
+ const { loginPath, rootPath } = admin.options
18
+ const normalizedLoginPath = loginPath.replace(rootPath, '')
19
+ return normalizedLoginPath.startsWith('/')
20
+ ? normalizedLoginPath
21
+ : `/${normalizedLoginPath}`
22
+ }
23
+
24
+ /**
25
+ * Manages login retry attempts per IP address
26
+ */
27
+ class Retry {
28
+ private static retriesContainer: Map<string, Retry> = new Map()
29
+ private lastRetry: Date | undefined
30
+ private retriesCount = 0
31
+
32
+ constructor(ip: string) {
33
+ const existing = Retry.retriesContainer.get(ip)
34
+ if (existing) {
35
+ return existing
36
+ }
37
+ Retry.retriesContainer.set(ip, this)
38
+ }
39
+
40
+ /**
41
+ * Checks if a login attempt is allowed based on retry limits
42
+ * @param maxRetries - Maximum retry configuration
43
+ * @returns true if login is allowed, false otherwise
44
+ */
45
+ public canLogin(
46
+ maxRetries: number | AuthenticationMaxRetriesOptions | undefined
47
+ ): boolean {
48
+ if (maxRetries === undefined) {
49
+ return true
50
+ }
51
+
52
+ // Convert number to AuthenticationMaxRetriesOptions
53
+ let retryConfig: AuthenticationMaxRetriesOptions
54
+ if (typeof maxRetries === 'number') {
55
+ retryConfig = {
56
+ count: maxRetries,
57
+ duration: 60, // per minute
58
+ }
59
+ } else {
60
+ retryConfig = maxRetries
61
+ }
62
+
63
+ if (retryConfig.count <= 0) {
64
+ return true
65
+ }
66
+
67
+ // Check if duration has passed since last retry
68
+ if (
69
+ !this.lastRetry ||
70
+ new Date().getTime() - this.lastRetry.getTime() >
71
+ retryConfig.duration * 1000
72
+ ) {
73
+ // Reset counter
74
+ this.lastRetry = new Date()
75
+ this.retriesCount = 1
76
+ return true
77
+ } else {
78
+ // Increment counter
79
+ this.lastRetry = new Date()
80
+ this.retriesCount++
81
+ return this.retriesCount <= retryConfig.count
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Registers login routes with the Hono app
88
+ * @param app - Hono app instance
89
+ * @param admin - AdminJS instance
90
+ * @param auth - Authentication options
91
+ */
92
+ export function withLogin(
93
+ app: Hono,
94
+ admin: AdminJS,
95
+ auth: AuthenticationOptions
96
+ ): void {
97
+ const { rootPath } = admin.options
98
+ const loginPath = getLoginPath(admin)
99
+
100
+ const { provider } = auth
101
+ const providerProps = provider?.getUiProps?.() ?? {}
102
+
103
+ // GET /login - Render login page
104
+ app.get(loginPath, async (c: Context<{ Variables: HonoVariables }>) => {
105
+ const baseProps = {
106
+ action: admin.options.loginPath,
107
+ errorMessage: null,
108
+ }
109
+ const login = await admin.renderLogin({
110
+ ...baseProps,
111
+ ...providerProps,
112
+ })
113
+
114
+ return c.html(login)
115
+ })
116
+
117
+ // POST /login - Handle login submission
118
+ app.post(loginPath, async (c: Context<{ Variables: HonoVariables }>) => {
119
+ // Get client IP for retry tracking
120
+ const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
121
+
122
+ // Check retry limits
123
+ if (!new Retry(ip).canLogin(auth.maxRetries)) {
124
+ const login = await admin.renderLogin({
125
+ action: admin.options.loginPath,
126
+ errorMessage: 'tooManyRequests',
127
+ ...providerProps,
128
+ })
129
+
130
+ return c.html(login)
131
+ }
132
+
133
+ const context: AuthenticationContext = { req: c, res: c }
134
+
135
+ let adminUser
136
+ try {
137
+ if (provider) {
138
+ // Use authentication provider
139
+ const fields = c.get('fields') || {}
140
+ adminUser = await provider.handleLogin(
141
+ {
142
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
143
+ query: c.req.query(),
144
+ params: c.req.param(),
145
+ data: fields,
146
+ },
147
+ context
148
+ )
149
+ } else if (auth.authenticate) {
150
+ // Use authenticate function
151
+ const fields = c.get('fields') || {}
152
+ const { email, password } = fields as {
153
+ email: string
154
+ password: string
155
+ }
156
+ adminUser = await auth.authenticate(email, password, context)
157
+ } else {
158
+ throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR)
159
+ }
160
+ } catch (error: any) {
161
+ const errorMessage = error.message || error.error || 'invalidCredentials'
162
+
163
+ const loginPage = await admin.renderLogin({
164
+ action: admin.options.loginPath,
165
+ errorMessage,
166
+ ...providerProps,
167
+ })
168
+
169
+ return c.html(loginPage, 400)
170
+ }
171
+
172
+ if (adminUser) {
173
+ // Store user in session
174
+ const session = c.get('session')
175
+ session.adminUser = adminUser
176
+
177
+ // Redirect to original path or root
178
+ const redirectTo = session.redirectTo || rootPath
179
+ delete session.redirectTo
180
+
181
+ return c.redirect(redirectTo, 302)
182
+ } else {
183
+ // Invalid credentials
184
+ const login = await admin.renderLogin({
185
+ action: admin.options.loginPath,
186
+ errorMessage: 'invalidCredentials',
187
+ ...providerProps,
188
+ })
189
+
190
+ return c.html(login)
191
+ }
192
+ })
193
+ }
@@ -0,0 +1,62 @@
1
+ import type AdminJS from 'adminjs'
2
+ import type { Hono, Context } from 'hono'
3
+ import { getCookie } from 'hono/cookie'
4
+ import type { AuthenticationOptions, HonoVariables } from '../types.js'
5
+ import { destroySession } from '../session.js'
6
+
7
+ /**
8
+ * Normalizes the logout path by removing the root path
9
+ * @param admin - AdminJS instance
10
+ * @returns Normalized logout path
11
+ */
12
+ function getLogoutPath(admin: AdminJS): string {
13
+ const { logoutPath, rootPath } = admin.options
14
+ const normalizedLogoutPath = logoutPath.replace(rootPath, '')
15
+ return normalizedLogoutPath.startsWith('/')
16
+ ? normalizedLogoutPath
17
+ : `/${normalizedLogoutPath}`
18
+ }
19
+
20
+ /**
21
+ * Registers logout route with the Hono app
22
+ * @param app - Hono app instance
23
+ * @param admin - AdminJS instance
24
+ * @param auth - Authentication options
25
+ */
26
+ export function withLogout(
27
+ app: Hono,
28
+ admin: AdminJS,
29
+ auth: AuthenticationOptions
30
+ ): void {
31
+ const logoutPath = getLogoutPath(admin)
32
+ const { provider } = auth
33
+
34
+ app.get(logoutPath, async (c: Context<{ Variables: HonoVariables }>) => {
35
+ // Call provider's handleLogout if available
36
+ if (provider) {
37
+ try {
38
+ await provider.handleLogout({ req: c, res: c })
39
+ } catch (error) {
40
+ // Fail silently and continue with logout
41
+ console.error('Provider logout error:', error)
42
+ }
43
+ }
44
+
45
+ // Get session ID and destroy session
46
+ const cookieName = auth.cookieName || 'adminjs'
47
+ const sessionId = getCookie(c, cookieName)
48
+ if (sessionId) {
49
+ destroySession(sessionId)
50
+ }
51
+
52
+ // Clear session from context
53
+ const session = c.get('session')
54
+ if (session) {
55
+ delete session.adminUser
56
+ delete session.redirectTo
57
+ }
58
+
59
+ // Redirect to login page
60
+ return c.redirect(admin.options.loginPath)
61
+ })
62
+ }
@@ -0,0 +1,38 @@
1
+ import type AdminJS from 'adminjs'
2
+ import type { Hono, MiddlewareHandler, Context } from 'hono'
3
+ import type { HonoVariables } from '../types.js'
4
+
5
+ /**
6
+ * Registers middleware to protect routes that require authentication
7
+ * Redirects unauthenticated requests to the login page
8
+ *
9
+ * @param app - Hono app instance
10
+ * @param admin - AdminJS instance
11
+ */
12
+ export function withProtectedRoutesHandler(
13
+ app: Hono,
14
+ admin: AdminJS
15
+ ): void {
16
+ const { loginPath } = admin.options
17
+
18
+ const authorizedRoutesMiddleware: MiddlewareHandler<{ Variables: HonoVariables }> = async (c: Context<{ Variables: HonoVariables }>, next) => {
19
+ const session = c.get('session')
20
+
21
+ // Check if user is authenticated
22
+ if (!session || !session.adminUser) {
23
+ // Store the original path for redirect after login
24
+ session.redirectTo = c.req.path
25
+
26
+ // Redirect to login page
27
+ return c.redirect(loginPath)
28
+ }
29
+
30
+ // User is authenticated, proceed
31
+ return next()
32
+ }
33
+
34
+ // Apply middleware to all routes
35
+ // Note: This should be registered after login/logout routes
36
+ // so they remain accessible without authentication
37
+ app.use('*', authorizedRoutesMiddleware)
38
+ }
@@ -0,0 +1,59 @@
1
+ import type AdminJS from 'adminjs'
2
+ import type { Hono, Context } from 'hono'
3
+ import type { AuthenticationOptions, HonoVariables } from '../types.js'
4
+
5
+ /**
6
+ * Registers token refresh route with the Hono app
7
+ * Delegates to authentication provider's refresh method if available
8
+ *
9
+ * @param app - Hono app instance
10
+ * @param admin - AdminJS instance
11
+ * @param auth - Authentication options
12
+ */
13
+ export function withRefresh(
14
+ app: Hono,
15
+ admin: AdminJS,
16
+ auth: AuthenticationOptions
17
+ ): void {
18
+ const { provider } = auth
19
+
20
+ // Only register refresh route if provider supports it
21
+ if (!provider || !(provider as any).handleRefresh) {
22
+ return
23
+ }
24
+
25
+ // Typically refresh is at /refresh, but this depends on the provider
26
+ const refreshPath = '/refresh'
27
+
28
+ app.post(refreshPath, async (c: Context<{ Variables: HonoVariables }>) => {
29
+ try {
30
+ const fields = c.get('fields') || {}
31
+ const session = c.get('session')
32
+
33
+ // Call provider's refresh method
34
+ const refreshedUser = await (provider as any).handleRefresh(
35
+ {
36
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
37
+ query: c.req.query(),
38
+ params: c.req.param(),
39
+ data: fields,
40
+ },
41
+ { req: c, res: c }
42
+ )
43
+
44
+ if (refreshedUser) {
45
+ // Update session with new credentials
46
+ session.adminUser = refreshedUser
47
+ return c.json({ success: true })
48
+ } else {
49
+ return c.json({ success: false, error: 'Refresh failed' }, 401)
50
+ }
51
+ } catch (error: any) {
52
+ console.error('Token refresh error:', error)
53
+ return c.json(
54
+ { success: false, error: error.message || 'Refresh failed' },
55
+ 401
56
+ )
57
+ }
58
+ })
59
+ }
@@ -0,0 +1,92 @@
1
+ import AdminJS, { Router as AdminRouter } from 'adminjs'
2
+ import type { Hono } from 'hono'
3
+ import { withLogin } from './authentication/login.handler.js'
4
+ import { withLogout } from './authentication/logout.handler.js'
5
+ import { withProtectedRoutesHandler } from './authentication/protected-routes.handler.js'
6
+ import { withRefresh } from './authentication/refresh.handler.js'
7
+ import { buildAssets, buildRoutes, initializeAdmin } from './buildRouter.js'
8
+ import {
9
+ INVALID_AUTH_CONFIG_ERROR,
10
+ MISSING_AUTH_CONFIG_ERROR,
11
+ WrongArgumentError,
12
+ } from './errors.js'
13
+ import type { AuthenticationOptions, SessionOptions, UploadOptions } from './types.js'
14
+ import { createFormParserMiddleware } from './formParser.js'
15
+ import { createSessionMiddleware } from './session.js'
16
+
17
+ /**
18
+ * Builds a Hono app with AdminJS routes protected by session-based authentication
19
+ *
20
+ * @param admin - The AdminJS instance
21
+ * @param auth - Authentication configuration
22
+ * @param predefinedApp - Optional existing Hono app to use
23
+ * @param sessionOptions - Optional session configuration
24
+ * @param uploadOptions - Optional upload configuration
25
+ * @returns Configured Hono app with authentication
26
+ */
27
+ export function buildAuthenticatedRouter(
28
+ admin: AdminJS,
29
+ auth: AuthenticationOptions,
30
+ predefinedApp?: Hono,
31
+ sessionOptions?: SessionOptions,
32
+ uploadOptions?: UploadOptions
33
+ ): Hono {
34
+ // Initialize AdminJS
35
+ initializeAdmin(admin)
36
+
37
+ // Validate authentication configuration
38
+ if (!auth.authenticate && !auth.provider) {
39
+ throw new WrongArgumentError(MISSING_AUTH_CONFIG_ERROR)
40
+ }
41
+
42
+ if (auth.authenticate && auth.provider) {
43
+ throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR)
44
+ }
45
+
46
+ // Use provided app or create new Hono instance
47
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
48
+ const { Hono: HonoClass } = require('hono')
49
+ const app = predefinedApp ?? new HonoClass()
50
+
51
+ // Get routes and assets from AdminJS
52
+ const { routes, assets } = AdminRouter
53
+
54
+ // If provider is configured, add its UI props to AdminJS env
55
+ if (auth.provider) {
56
+ admin.options.env = {
57
+ ...admin.options.env,
58
+ ...auth.provider.getUiProps(),
59
+ }
60
+ }
61
+
62
+ // Configure session middleware
63
+ const cookieName = auth.cookieName || 'adminjs'
64
+ app.use('*', createSessionMiddleware(
65
+ auth.cookiePassword,
66
+ cookieName,
67
+ sessionOptions
68
+ ))
69
+
70
+ // Register form parsing middleware
71
+ app.use('*', createFormParserMiddleware(uploadOptions))
72
+
73
+ // Register login handler (must be before protected routes middleware)
74
+ withLogin(app, admin, auth)
75
+
76
+ // Register logout handler (must be before protected routes middleware)
77
+ withLogout(app, admin, auth)
78
+
79
+ // Build assets (must be before protected routes middleware)
80
+ buildAssets(assets, routes, app, admin)
81
+
82
+ // Register protected routes middleware (applies to all subsequent routes)
83
+ withProtectedRoutesHandler(app, admin)
84
+
85
+ // Register refresh handler (after protected routes middleware)
86
+ withRefresh(app, admin, auth)
87
+
88
+ // Build routes (after protected routes middleware)
89
+ buildRoutes(routes, app, admin)
90
+
91
+ return app
92
+ }