@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
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
|
+
}
|