@mantiq/auth 0.2.1 → 0.3.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/package.json +1 -1
- package/src/AuthServiceProvider.ts +27 -2
- package/src/Authorizable.ts +22 -0
- package/src/authorization/AuthorizationResponse.ts +35 -0
- package/src/authorization/GateManager.ts +247 -0
- package/src/authorization/Policy.ts +18 -0
- package/src/authorization/UserGate.ts +28 -0
- package/src/contracts/AuthConfig.ts +1 -0
- package/src/helpers/gate.ts +22 -0
- package/src/index.ts +9 -0
- package/src/middleware/Authorize.ts +41 -0
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { ServiceProvider, ConfigRepository } from '@mantiq/core'
|
|
1
|
+
import { ServiceProvider, ConfigRepository, HttpKernel } from '@mantiq/core'
|
|
2
2
|
import { AuthManager } from './AuthManager.ts'
|
|
3
3
|
import { AUTH_MANAGER } from './helpers/auth.ts'
|
|
4
4
|
import { Authenticate } from './middleware/Authenticate.ts'
|
|
5
5
|
import { RedirectIfAuthenticated } from './middleware/RedirectIfAuthenticated.ts'
|
|
6
6
|
import { EnsureEmailIsVerified } from './middleware/EnsureEmailIsVerified.ts'
|
|
7
7
|
import { ConfirmPassword } from './middleware/ConfirmPassword.ts'
|
|
8
|
+
import { Authorize } from './middleware/Authorize.ts'
|
|
9
|
+
import { GateManager } from './authorization/GateManager.ts'
|
|
10
|
+
import { setGateManager, GATE_MANAGER } from './helpers/gate.ts'
|
|
8
11
|
import type { AuthConfig } from './contracts/AuthConfig.ts'
|
|
9
12
|
|
|
10
13
|
const DEFAULT_CONFIG: AuthConfig = {
|
|
@@ -18,7 +21,7 @@ const DEFAULT_CONFIG: AuthConfig = {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
|
-
* Registers authentication bindings in the container.
|
|
24
|
+
* Registers authentication and authorization bindings in the container.
|
|
22
25
|
*
|
|
23
26
|
* Config file: config/auth.ts
|
|
24
27
|
* Required config: guards, providers, defaults.guard
|
|
@@ -32,10 +35,32 @@ export class AuthServiceProvider extends ServiceProvider {
|
|
|
32
35
|
})
|
|
33
36
|
this.app.alias(AuthManager, AUTH_MANAGER)
|
|
34
37
|
|
|
38
|
+
// GateManager — singleton
|
|
39
|
+
this.app.singleton(GateManager, () => {
|
|
40
|
+
const g = new GateManager()
|
|
41
|
+
setGateManager(g)
|
|
42
|
+
return g
|
|
43
|
+
})
|
|
44
|
+
this.app.alias(GateManager, GATE_MANAGER)
|
|
45
|
+
|
|
35
46
|
// Middleware bindings
|
|
36
47
|
this.app.bind(Authenticate, (c) => new Authenticate(c.make(AuthManager)))
|
|
37
48
|
this.app.bind(RedirectIfAuthenticated, (c) => new RedirectIfAuthenticated(c.make(AuthManager)))
|
|
38
49
|
this.app.bind(EnsureEmailIsVerified, () => new EnsureEmailIsVerified())
|
|
39
50
|
this.app.bind(ConfirmPassword, () => new ConfirmPassword())
|
|
51
|
+
this.app.bind(Authorize, () => new Authorize())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override boot(): void {
|
|
55
|
+
// Resolve the GateManager so setGateManager() is called
|
|
56
|
+
this.app.make(GateManager)
|
|
57
|
+
|
|
58
|
+
// Register the 'can' middleware alias
|
|
59
|
+
try {
|
|
60
|
+
const kernel = this.app.make(HttpKernel)
|
|
61
|
+
kernel.registerMiddleware('can', Authorize)
|
|
62
|
+
} catch {
|
|
63
|
+
// HttpKernel may not be available in non-HTTP contexts (e.g., CLI)
|
|
64
|
+
}
|
|
40
65
|
}
|
|
41
66
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { gate } from './helpers/gate.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mixin that adds `can()` / `cannot()` to model instances.
|
|
5
|
+
*
|
|
6
|
+
* Apply to any model class to allow authorization checks directly
|
|
7
|
+
* on the model instance:
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* applyAuthorizable(User)
|
|
11
|
+
* const user = await User.find(1)
|
|
12
|
+
* if (await user.can('edit', post)) { ... }
|
|
13
|
+
*/
|
|
14
|
+
export function applyAuthorizable(ModelClass: any): void {
|
|
15
|
+
ModelClass.prototype.can = async function (ability: string, ...args: any[]): Promise<boolean> {
|
|
16
|
+
return gate().allows(ability, this, ...args)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ModelClass.prototype.cannot = async function (ability: string, ...args: any[]): Promise<boolean> {
|
|
20
|
+
return gate().denies(ability, this, ...args)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class AuthorizationResponse {
|
|
2
|
+
private _allowed: boolean
|
|
3
|
+
private _message: string | null
|
|
4
|
+
private _code: number | null
|
|
5
|
+
|
|
6
|
+
constructor(allowed: boolean, message?: string, code?: number) {
|
|
7
|
+
this._allowed = allowed
|
|
8
|
+
this._message = message ?? null
|
|
9
|
+
this._code = code ?? null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
allowed(): boolean {
|
|
13
|
+
return this._allowed
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
denied(): boolean {
|
|
17
|
+
return !this._allowed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
message(): string | null {
|
|
21
|
+
return this._message
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
code(): number | null {
|
|
25
|
+
return this._code
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static allow(message?: string): AuthorizationResponse {
|
|
29
|
+
return new AuthorizationResponse(true, message)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static deny(message?: string, code?: number): AuthorizationResponse {
|
|
33
|
+
return new AuthorizationResponse(false, message ?? 'This action is unauthorized.', code ?? 403)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { ForbiddenError } from '@mantiq/core'
|
|
2
|
+
import { AuthorizationResponse } from './AuthorizationResponse.ts'
|
|
3
|
+
import type { Policy } from './Policy.ts'
|
|
4
|
+
import { UserGate } from './UserGate.ts'
|
|
5
|
+
|
|
6
|
+
type GateCallback = (user: any, ...args: any[]) => boolean | AuthorizationResponse | Promise<boolean | AuthorizationResponse>
|
|
7
|
+
type BeforeCallback = (user: any, ability: string, ...args: any[]) => boolean | null | undefined | Promise<boolean | null | undefined>
|
|
8
|
+
type AfterCallback = (user: any, ability: string, result: boolean, ...args: any[]) => void | Promise<void>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Central authorization manager — Laravel-style Gates & Policies.
|
|
12
|
+
*
|
|
13
|
+
* Gate closures handle simple ability checks.
|
|
14
|
+
* Policies handle model-based authorization — each policy maps to a model class.
|
|
15
|
+
*
|
|
16
|
+
* Resolution order:
|
|
17
|
+
* 1. Global `before` callbacks (short-circuit on non-null)
|
|
18
|
+
* 2. Policy `before()` hook (if a policy matches)
|
|
19
|
+
* 3. Policy method matching the ability name
|
|
20
|
+
* 4. Gate closure matching the ability name
|
|
21
|
+
* 5. Deny by default
|
|
22
|
+
* 6. Global `after` callbacks (for auditing, cannot change result)
|
|
23
|
+
*/
|
|
24
|
+
export class GateManager {
|
|
25
|
+
private gates = new Map<string, GateCallback>()
|
|
26
|
+
private policies = new Map<any, new () => Policy>()
|
|
27
|
+
private beforeCallbacks: BeforeCallback[] = []
|
|
28
|
+
private afterCallbacks: AfterCallback[] = []
|
|
29
|
+
|
|
30
|
+
// ── Registration ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Define a gate closure for the given ability.
|
|
34
|
+
*/
|
|
35
|
+
define(ability: string, callback: GateCallback): this {
|
|
36
|
+
this.gates.set(ability, callback)
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register a policy class for a model constructor.
|
|
42
|
+
*/
|
|
43
|
+
policy(modelClass: any, policyClass: new () => Policy): this {
|
|
44
|
+
this.policies.set(modelClass, policyClass)
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register a global before callback.
|
|
50
|
+
* If the callback returns `true` or `false`, the check short-circuits.
|
|
51
|
+
* Return `null` or `undefined` to continue to the gate/policy check.
|
|
52
|
+
*/
|
|
53
|
+
before(callback: BeforeCallback): this {
|
|
54
|
+
this.beforeCallbacks.push(callback)
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register a global after callback (for logging/auditing).
|
|
60
|
+
* After callbacks cannot change the authorization result.
|
|
61
|
+
*/
|
|
62
|
+
after(callback: AfterCallback): this {
|
|
63
|
+
this.afterCallbacks.push(callback)
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Checking ─────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if the given ability is allowed for the user.
|
|
71
|
+
*/
|
|
72
|
+
async allows(ability: string, user: any, ...args: any[]): Promise<boolean> {
|
|
73
|
+
const response = await this.resolve(ability, user, ...args)
|
|
74
|
+
return response.allowed()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if the given ability is denied for the user.
|
|
79
|
+
*/
|
|
80
|
+
async denies(ability: string, user: any, ...args: any[]): Promise<boolean> {
|
|
81
|
+
return !(await this.allows(ability, user, ...args))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Authorize the ability or throw ForbiddenError.
|
|
86
|
+
* Returns the AuthorizationResponse if allowed.
|
|
87
|
+
*/
|
|
88
|
+
async authorize(ability: string, user: any, ...args: any[]): Promise<AuthorizationResponse> {
|
|
89
|
+
const response = await this.resolve(ability, user, ...args)
|
|
90
|
+
|
|
91
|
+
if (response.denied()) {
|
|
92
|
+
throw new ForbiddenError(response.message() ?? 'This action is unauthorized.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return response
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check multiple abilities — all must pass.
|
|
100
|
+
*/
|
|
101
|
+
async check(abilities: string[], user: any, ...args: any[]): Promise<boolean> {
|
|
102
|
+
for (const ability of abilities) {
|
|
103
|
+
if (!(await this.allows(ability, user, ...args))) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check multiple abilities — at least one must pass.
|
|
112
|
+
*/
|
|
113
|
+
async any(abilities: string[], user: any, ...args: any[]): Promise<boolean> {
|
|
114
|
+
for (const ability of abilities) {
|
|
115
|
+
if (await this.allows(ability, user, ...args)) {
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns a UserGate scoped to a specific user for convenience.
|
|
124
|
+
*/
|
|
125
|
+
forUser(user: any): UserGate {
|
|
126
|
+
return new UserGate(this, user)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Policy resolution ──────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the policy class for a given model instance.
|
|
133
|
+
* Returns null if no policy is registered for this model's constructor.
|
|
134
|
+
*/
|
|
135
|
+
getPolicyFor(model: any): Policy | null {
|
|
136
|
+
if (model === null || model === undefined) return null
|
|
137
|
+
|
|
138
|
+
const constructor = model.constructor
|
|
139
|
+
const PolicyClass = this.policies.get(constructor)
|
|
140
|
+
|
|
141
|
+
if (!PolicyClass) return null
|
|
142
|
+
return new PolicyClass()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Internal resolution ────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Core resolution logic.
|
|
149
|
+
*/
|
|
150
|
+
private async resolve(ability: string, user: any, ...args: any[]): Promise<AuthorizationResponse> {
|
|
151
|
+
// 1. Run global before callbacks
|
|
152
|
+
for (const cb of this.beforeCallbacks) {
|
|
153
|
+
const result = await cb(user, ability, ...args)
|
|
154
|
+
if (result === true) {
|
|
155
|
+
const response = AuthorizationResponse.allow()
|
|
156
|
+
await this.runAfterCallbacks(user, ability, true, ...args)
|
|
157
|
+
return response
|
|
158
|
+
}
|
|
159
|
+
if (result === false) {
|
|
160
|
+
const response = AuthorizationResponse.deny()
|
|
161
|
+
await this.runAfterCallbacks(user, ability, false, ...args)
|
|
162
|
+
return response
|
|
163
|
+
}
|
|
164
|
+
// null/undefined → continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. Try policy-based authorization
|
|
168
|
+
const firstArg = args[0]
|
|
169
|
+
const policyResult = await this.tryPolicy(ability, user, firstArg, ...args)
|
|
170
|
+
if (policyResult !== null) {
|
|
171
|
+
const allowed = policyResult.allowed()
|
|
172
|
+
await this.runAfterCallbacks(user, ability, allowed, ...args)
|
|
173
|
+
return policyResult
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 3. Try gate closure
|
|
177
|
+
const gateResult = await this.tryGate(ability, user, ...args)
|
|
178
|
+
if (gateResult !== null) {
|
|
179
|
+
const allowed = gateResult.allowed()
|
|
180
|
+
await this.runAfterCallbacks(user, ability, allowed, ...args)
|
|
181
|
+
return gateResult
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 4. Deny by default
|
|
185
|
+
const denied = AuthorizationResponse.deny()
|
|
186
|
+
await this.runAfterCallbacks(user, ability, false, ...args)
|
|
187
|
+
return denied
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Attempt policy-based authorization.
|
|
192
|
+
* Returns null if no matching policy or method.
|
|
193
|
+
*/
|
|
194
|
+
private async tryPolicy(ability: string, user: any, firstArg: any, ...allArgs: any[]): Promise<AuthorizationResponse | null> {
|
|
195
|
+
if (firstArg === null || firstArg === undefined) return null
|
|
196
|
+
|
|
197
|
+
const policy = this.getPolicyFor(firstArg)
|
|
198
|
+
if (!policy) return null
|
|
199
|
+
|
|
200
|
+
// Policy before() hook
|
|
201
|
+
if (typeof policy.before === 'function') {
|
|
202
|
+
const beforeResult = await policy.before(user, ability, ...allArgs)
|
|
203
|
+
if (beforeResult === true) return AuthorizationResponse.allow()
|
|
204
|
+
if (beforeResult === false) return AuthorizationResponse.deny()
|
|
205
|
+
// null/undefined → continue to method
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check if the policy has a method matching the ability name
|
|
209
|
+
const method = (policy as any)[ability]
|
|
210
|
+
if (typeof method !== 'function') {
|
|
211
|
+
// Policy exists but no matching method → deny
|
|
212
|
+
return AuthorizationResponse.deny(`Policy method "${ability}" not defined.`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const result = await method.call(policy, user, ...allArgs)
|
|
216
|
+
return this.normalizeResult(result)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Attempt gate closure authorization.
|
|
221
|
+
* Returns null if no gate is defined for this ability.
|
|
222
|
+
*/
|
|
223
|
+
private async tryGate(ability: string, user: any, ...args: any[]): Promise<AuthorizationResponse | null> {
|
|
224
|
+
const callback = this.gates.get(ability)
|
|
225
|
+
if (!callback) return null
|
|
226
|
+
|
|
227
|
+
const result = await callback(user, ...args)
|
|
228
|
+
return this.normalizeResult(result)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Normalize a boolean or AuthorizationResponse return value.
|
|
233
|
+
*/
|
|
234
|
+
private normalizeResult(result: boolean | AuthorizationResponse): AuthorizationResponse {
|
|
235
|
+
if (result instanceof AuthorizationResponse) return result
|
|
236
|
+
return result ? AuthorizationResponse.allow() : AuthorizationResponse.deny()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Run all global after callbacks.
|
|
241
|
+
*/
|
|
242
|
+
private async runAfterCallbacks(user: any, ability: string, result: boolean, ...args: any[]): Promise<void> {
|
|
243
|
+
for (const cb of this.afterCallbacks) {
|
|
244
|
+
await cb(user, ability, result, ...args)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for authorization policies.
|
|
3
|
+
*
|
|
4
|
+
* Subclass and define methods matching ability names.
|
|
5
|
+
* Each method receives the user + optional model arguments and returns
|
|
6
|
+
* a boolean or AuthorizationResponse.
|
|
7
|
+
*
|
|
8
|
+
* The optional `before()` hook is called before any policy method and
|
|
9
|
+
* can short-circuit the check: return `true` to allow, `false` to deny,
|
|
10
|
+
* or `null`/`undefined` to continue to the actual policy method.
|
|
11
|
+
*/
|
|
12
|
+
export abstract class Policy {
|
|
13
|
+
/**
|
|
14
|
+
* Called before any policy method.
|
|
15
|
+
* Return true to allow, false to deny, null/undefined to continue.
|
|
16
|
+
*/
|
|
17
|
+
before?(user: any, ability: string, ...args: any[]): boolean | null | undefined | Promise<boolean | null | undefined>
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GateManager } from './GateManager.ts'
|
|
2
|
+
import type { AuthorizationResponse } from './AuthorizationResponse.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A gate scoped to a specific user for convenience.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const userGate = gate().forUser(user)
|
|
9
|
+
* if (await userGate.can('edit', post)) { ... }
|
|
10
|
+
*/
|
|
11
|
+
export class UserGate {
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly gate: GateManager,
|
|
14
|
+
private readonly user: any,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async can(ability: string, ...args: any[]): Promise<boolean> {
|
|
18
|
+
return this.gate.allows(ability, this.user, ...args)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async cannot(ability: string, ...args: any[]): Promise<boolean> {
|
|
22
|
+
return this.gate.denies(ability, this.user, ...args)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async authorize(ability: string, ...args: any[]): Promise<AuthorizationResponse> {
|
|
26
|
+
return this.gate.authorize(ability, this.user, ...args)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { Constructor } from '@mantiq/core'
|
|
|
3
3
|
export interface GuardConfig {
|
|
4
4
|
driver: string // 'session' | custom driver name
|
|
5
5
|
provider: string // Name referencing a provider in config.providers
|
|
6
|
+
trackLastUsed?: boolean | undefined
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export interface ProviderConfig {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GateManager } from '../authorization/GateManager.ts'
|
|
2
|
+
|
|
3
|
+
let _gate: GateManager | null = null
|
|
4
|
+
|
|
5
|
+
export const GATE_MANAGER = Symbol('GateManager')
|
|
6
|
+
|
|
7
|
+
export function setGateManager(g: GateManager): void {
|
|
8
|
+
_gate = g
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Access the gate manager singleton.
|
|
13
|
+
*
|
|
14
|
+
* @example gate().allows('edit-post', user, post)
|
|
15
|
+
* @example gate().forUser(user).can('edit-post', post)
|
|
16
|
+
*/
|
|
17
|
+
export function gate(): GateManager {
|
|
18
|
+
if (!_gate) {
|
|
19
|
+
throw new Error('Gate manager not initialized. Register AuthServiceProvider.')
|
|
20
|
+
}
|
|
21
|
+
return _gate
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type { NewAccessToken } from './contracts/NewAccessToken.ts'
|
|
|
10
10
|
export { AuthManager } from './AuthManager.ts'
|
|
11
11
|
export { AuthServiceProvider } from './AuthServiceProvider.ts'
|
|
12
12
|
|
|
13
|
+
// ── Authorization (Gates & Policies) ─────────────────────────────────────────
|
|
14
|
+
export { GateManager } from './authorization/GateManager.ts'
|
|
15
|
+
export { AuthorizationResponse } from './authorization/AuthorizationResponse.ts'
|
|
16
|
+
export { Policy } from './authorization/Policy.ts'
|
|
17
|
+
export { UserGate } from './authorization/UserGate.ts'
|
|
18
|
+
|
|
13
19
|
// ── Guards ────────────────────────────────────────────────────────────────────
|
|
14
20
|
export { SessionGuard } from './guards/SessionGuard.ts'
|
|
15
21
|
export { RequestGuard } from './guards/RequestGuard.ts'
|
|
@@ -25,6 +31,7 @@ export { EnsureEmailIsVerified } from './middleware/EnsureEmailIsVerified.ts'
|
|
|
25
31
|
export { ConfirmPassword } from './middleware/ConfirmPassword.ts'
|
|
26
32
|
export { CheckAbilities } from './middleware/CheckAbilities.ts'
|
|
27
33
|
export { CheckForAnyAbility } from './middleware/CheckForAnyAbility.ts'
|
|
34
|
+
export { Authorize } from './middleware/Authorize.ts'
|
|
28
35
|
|
|
29
36
|
// ── Errors ────────────────────────────────────────────────────────────────────
|
|
30
37
|
export { AuthenticationError } from './errors/AuthenticationError.ts'
|
|
@@ -37,6 +44,7 @@ export { PersonalAccessToken } from './models/PersonalAccessToken.ts'
|
|
|
37
44
|
|
|
38
45
|
// ── Mixins ────────────────────────────────────────────────────────────────────
|
|
39
46
|
export { applyHasApiTokens } from './HasApiTokens.ts'
|
|
47
|
+
export { applyAuthorizable } from './Authorizable.ts'
|
|
40
48
|
|
|
41
49
|
// ── Migrations ────────────────────────────────────────────────────────────────
|
|
42
50
|
export { CreatePersonalAccessTokensTable } from './migrations/CreatePersonalAccessTokensTable.ts'
|
|
@@ -44,3 +52,4 @@ export { CreatePersonalAccessTokensTable } from './migrations/CreatePersonalAcce
|
|
|
44
52
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
45
53
|
export { sha256 } from './helpers/hash.ts'
|
|
46
54
|
export { auth, AUTH_MANAGER } from './helpers/auth.ts'
|
|
55
|
+
export { gate, setGateManager, GATE_MANAGER } from './helpers/gate.ts'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Middleware, NextFunction, MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import { gate } from '../helpers/gate.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authorization middleware for routes.
|
|
6
|
+
*
|
|
7
|
+
* Checks that the authenticated user is authorized for the given ability.
|
|
8
|
+
* Returns 401 if unauthenticated, or throws ForbiddenError (via gate().authorize())
|
|
9
|
+
* if the user is not authorized.
|
|
10
|
+
*
|
|
11
|
+
* Usage: `.middleware('can:update,post')`
|
|
12
|
+
*
|
|
13
|
+
* - params[0] = ability name
|
|
14
|
+
* - params[1..] = optional extra arguments passed to the gate/policy
|
|
15
|
+
*/
|
|
16
|
+
export class Authorize implements Middleware {
|
|
17
|
+
private params: string[] = []
|
|
18
|
+
|
|
19
|
+
setParameters(params: string[]): void {
|
|
20
|
+
this.params = params
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async handle(request: MantiqRequest, next: NextFunction): Promise<Response> {
|
|
24
|
+
const user = request.user()
|
|
25
|
+
if (!user) {
|
|
26
|
+
return new Response(
|
|
27
|
+
JSON.stringify({ message: 'Unauthenticated.' }),
|
|
28
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ability = this.params[0]
|
|
33
|
+
if (!ability) return next()
|
|
34
|
+
|
|
35
|
+
const gateManager = gate()
|
|
36
|
+
// authorize() throws ForbiddenError if denied
|
|
37
|
+
await gateManager.authorize(ability, user, ...this.params.slice(1))
|
|
38
|
+
|
|
39
|
+
return next()
|
|
40
|
+
}
|
|
41
|
+
}
|