Skip to content

Commit c4e93af

Browse files
committed
feat: add auth, custom decorators and custom plugins
1 parent 9b65742 commit c4e93af

9 files changed

Lines changed: 1251 additions & 11 deletions

File tree

content/docs/3.modules/1.auth.md

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
title: Auth
2+
icon: false
3+
defaultOpen: false
4+
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
---
2+
title: Overview
3+
description: Flexible authentication system for Vercube applications
4+
---
5+
6+
The Auth module provides a powerful, flexible authentication system for Vercube applications. Built around a provider-based architecture, it allows you to implement various authentication strategies such as JWT, sessions, OAuth, or custom solutions.
7+
8+
## Installation
9+
10+
::code-group
11+
```bash [pnpm]
12+
$ pnpm add @vercube/auth
13+
```
14+
```bash [npm]
15+
$ npm install @vercube/auth
16+
```
17+
```bash [bun]
18+
$ bun install @vercube/auth
19+
```
20+
::
21+
22+
## Quick Start
23+
24+
::steps
25+
### Create an Auth Provider
26+
27+
Create a custom authentication provider by extending the `AuthProvider` class:
28+
29+
```ts [src/providers/JWTAuthProvider.ts]
30+
import { AuthProvider, type AuthTypes } from '@vercube/auth';
31+
32+
interface User {
33+
id: number;
34+
username: string;
35+
roles: string[];
36+
}
37+
38+
export class JWTAuthProvider extends AuthProvider<User> {
39+
40+
public validate(request: Request, params?: AuthTypes.MiddlewareOptions): string | null {
41+
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
42+
43+
if (!token) {
44+
return 'No token provided';
45+
}
46+
47+
try {
48+
const user = this.verifyToken(token);
49+
50+
// Check roles if specified
51+
if (params?.roles && params.roles.length > 0) {
52+
const hasRequiredRole = params.roles.some(role => user.roles.includes(role));
53+
if (!hasRequiredRole) {
54+
return 'Insufficient permissions';
55+
}
56+
}
57+
58+
return null; // Authentication successful
59+
} catch {
60+
return 'Invalid token';
61+
}
62+
}
63+
64+
public getCurrentUser(request: Request): User | null {
65+
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
66+
67+
if (!token) {
68+
return null;
69+
}
70+
71+
try {
72+
return this.verifyToken(token);
73+
} catch {
74+
return null;
75+
}
76+
}
77+
78+
private verifyToken(token: string): User {
79+
// Your JWT verification logic here
80+
// This is a simplified example
81+
return { id: 1, username: 'john', roles: ['user'] };
82+
}
83+
}
84+
```
85+
86+
### Register Provider in Container
87+
88+
Register your auth provider in the DI container during application setup:
89+
90+
```ts [src/setup.ts]
91+
import { type App } from '@vercube/core';
92+
import { AuthProvider } from '@vercube/auth';
93+
import { JWTAuthProvider } from './providers/JWTAuthProvider';
94+
95+
export function setup(app: App): void {
96+
app.container.bind(AuthProvider, JWTAuthProvider);
97+
}
98+
```
99+
100+
### Protect Your Endpoints
101+
102+
Use the `@Auth` decorator to protect controller methods:
103+
104+
```ts [src/controllers/ProfileController.ts]
105+
import { Controller, Get } from '@vercube/core';
106+
import { Auth, User } from '@vercube/auth';
107+
108+
interface User {
109+
id: number;
110+
username: string;
111+
roles: string[];
112+
}
113+
114+
@Controller('/profile')
115+
export class ProfileController {
116+
117+
@Get('/')
118+
@Auth()
119+
public getProfile(@User() user: User) {
120+
return { profile: user };
121+
}
122+
123+
@Get('/admin')
124+
@Auth({ roles: ['admin'] })
125+
public getAdminPanel(@User() user: User) {
126+
return { admin: true, user };
127+
}
128+
}
129+
```
130+
::
131+
132+
## Core Concepts
133+
134+
### AuthProvider
135+
136+
The `AuthProvider` is an abstract class that defines the interface for authentication implementations. All authentication providers must extend this class and implement two methods:
137+
138+
- **`validate()`** - Validates incoming requests and returns `null` on success or an error message string on failure
139+
- **`getCurrentUser()`** - Returns the authenticated user object or `null` if not authenticated
140+
141+
### Decorators
142+
143+
The Auth module provides two decorators for easy integration with controllers:
144+
145+
| Decorator | Description |
146+
|-----------|-------------|
147+
| `@Auth()` | Protects a method, requiring authentication before execution |
148+
| `@User()` | Injects the current authenticated user as a method parameter |
149+
150+
### Role-Based Access Control
151+
152+
You can restrict access based on user roles by passing options to the `@Auth` decorator:
153+
154+
```ts
155+
@Auth({ roles: ['admin', 'moderator'] })
156+
public adminOnly(@User() user: User) {
157+
// Only accessible by admins and moderators
158+
}
159+
```
160+
161+
The `validate()` method in your provider receives these options and should check if the user has the required roles.
162+
163+
## Authentication Flow
164+
165+
When a request hits a protected endpoint:
166+
167+
1. The `@Auth` decorator triggers the authentication middleware
168+
2. Your `AuthProvider.validate()` method is called with the request
169+
3. If `validate()` returns `null`, authentication succeeds
170+
4. If `validate()` returns a string, authentication fails with that error message
171+
5. The `@User` decorator calls `getCurrentUser()` to inject the user object
172+
173+
<img src="/images/auth-1.svg" alt="Authentication Flow"/>
174+
175+
## Common Patterns
176+
177+
::code-group
178+
```ts [JWT]
179+
import { AuthProvider, type AuthTypes } from '@vercube/auth';
180+
import jwt from 'jsonwebtoken';
181+
182+
export class JWTAuthProvider extends AuthProvider<User> {
183+
private secret = process.env.JWT_SECRET!;
184+
185+
public validate(request: Request, params?: AuthTypes.MiddlewareOptions): string | null {
186+
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
187+
188+
if (!token) {
189+
return 'Authorization header required';
190+
}
191+
192+
try {
193+
const decoded = jwt.verify(token, this.secret) as User;
194+
195+
if (params?.roles?.length) {
196+
if (!params.roles.some(role => decoded.roles.includes(role))) {
197+
return 'Insufficient permissions';
198+
}
199+
}
200+
201+
return null;
202+
} catch (error) {
203+
if (error instanceof jwt.TokenExpiredError) {
204+
return 'Token expired';
205+
}
206+
return 'Invalid token';
207+
}
208+
}
209+
210+
public getCurrentUser(request: Request): User | null {
211+
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
212+
if (!token) return null;
213+
214+
try {
215+
return jwt.verify(token, this.secret) as User;
216+
} catch {
217+
return null;
218+
}
219+
}
220+
}
221+
```
222+
```ts [API Key]
223+
import { AuthProvider, type AuthTypes } from '@vercube/auth';
224+
225+
interface ApiKeyUser {
226+
apiKeyId: string;
227+
permissions: string[];
228+
}
229+
230+
export class ApiKeyAuthProvider extends AuthProvider<ApiKeyUser> {
231+
private validKeys = new Map<string, ApiKeyUser>();
232+
233+
public validate(request: Request): string | null {
234+
const apiKey = request.headers.get('X-API-Key');
235+
236+
if (!apiKey) {
237+
return 'API key required';
238+
}
239+
240+
if (!this.validKeys.has(apiKey)) {
241+
return 'Invalid API key';
242+
}
243+
244+
return null;
245+
}
246+
247+
public getCurrentUser(request: Request): ApiKeyUser | null {
248+
const apiKey = request.headers.get('X-API-Key');
249+
return apiKey ? this.validKeys.get(apiKey) || null : null;
250+
}
251+
}
252+
```
253+
```ts [Session]
254+
import { AuthProvider } from '@vercube/auth';
255+
import { Inject } from '@vercube/di';
256+
257+
export class SessionAuthProvider extends AuthProvider<User> {
258+
@Inject(SessionStore)
259+
private sessions!: SessionStore;
260+
261+
public async validate(request: Request): Promise<string | null> {
262+
const sessionId = this.getSessionCookie(request);
263+
264+
if (!sessionId) {
265+
return 'Session required';
266+
}
267+
268+
const session = await this.sessions.get(sessionId);
269+
if (!session) {
270+
return 'Invalid or expired session';
271+
}
272+
273+
return null;
274+
}
275+
276+
public async getCurrentUser(request: Request): Promise<User | null> {
277+
const sessionId = this.getSessionCookie(request);
278+
if (!sessionId) return null;
279+
280+
const session = await this.sessions.get(sessionId);
281+
return session?.user || null;
282+
}
283+
284+
private getSessionCookie(request: Request): string | null {
285+
const cookies = request.headers.get('Cookie');
286+
// Parse session cookie...
287+
return null;
288+
}
289+
}
290+
```
291+
::
292+

0 commit comments

Comments
 (0)