@plyaz/auth 1.0.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/.github/pull_request_template.md +71 -0
- package/.github/workflows/deploy.yml +9 -0
- package/.github/workflows/publish.yml +14 -0
- package/.github/workflows/security.yml +20 -0
- package/README.md +89 -0
- package/commits.txt +5 -0
- package/dist/common/index.cjs +48 -0
- package/dist/common/index.cjs.map +1 -0
- package/dist/common/index.mjs +43 -0
- package/dist/common/index.mjs.map +1 -0
- package/dist/index.cjs +20411 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +5139 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +13 -0
- package/index.html +13 -0
- package/package.json +141 -0
- package/src/adapters/auth-adapter-factory.ts +26 -0
- package/src/adapters/auth-adapter.mapper.ts +53 -0
- package/src/adapters/base-auth.adapter.ts +119 -0
- package/src/adapters/clerk/clerk.adapter.ts +204 -0
- package/src/adapters/custom/custom.adapter.ts +119 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/next-auth/authOptions.ts +81 -0
- package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
- package/src/api/client.ts +37 -0
- package/src/audit/audit.logger.ts +52 -0
- package/src/client/components/ProtectedRoute.tsx +37 -0
- package/src/client/hooks/useAuth.ts +128 -0
- package/src/client/hooks/useConnectedAccounts.ts +108 -0
- package/src/client/hooks/usePermissions.ts +36 -0
- package/src/client/hooks/useRBAC.ts +36 -0
- package/src/client/hooks/useSession.ts +18 -0
- package/src/client/providers/AuthProvider.tsx +104 -0
- package/src/client/store/auth.store.ts +306 -0
- package/src/client/utils/storage.ts +70 -0
- package/src/common/constants/oauth-providers.ts +49 -0
- package/src/common/errors/auth.errors.ts +64 -0
- package/src/common/errors/specific-auth-errors.ts +201 -0
- package/src/common/index.ts +19 -0
- package/src/common/regex/index.ts +27 -0
- package/src/common/types/auth.types.ts +641 -0
- package/src/common/types/index.ts +297 -0
- package/src/common/utils/index.ts +84 -0
- package/src/core/blacklist/token.blacklist.ts +60 -0
- package/src/core/index.ts +2 -0
- package/src/core/jwt/jwt.manager.ts +131 -0
- package/src/core/session/session.manager.ts +56 -0
- package/src/db/repositories/connected-account.repository.ts +415 -0
- package/src/db/repositories/role.repository.ts +519 -0
- package/src/db/repositories/session.repository.ts +308 -0
- package/src/db/repositories/user.repository.ts +320 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/sign-in.flow.ts +106 -0
- package/src/flows/sign-up.flow.ts +121 -0
- package/src/index.ts +54 -0
- package/src/libs/clerk.helper.ts +36 -0
- package/src/libs/supabase.helper.ts +255 -0
- package/src/libs/supabaseClient.ts +6 -0
- package/src/providers/base/auth-provider.interface.ts +42 -0
- package/src/providers/base/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/oauth/facebook.provider.ts +97 -0
- package/src/providers/oauth/github.provider.ts +148 -0
- package/src/providers/oauth/google.provider.ts +126 -0
- package/src/providers/oauth/index.ts +3 -0
- package/src/rbac/dynamic-roles.ts +552 -0
- package/src/rbac/index.ts +4 -0
- package/src/rbac/permission-checker.ts +464 -0
- package/src/rbac/role-hierarchy.ts +545 -0
- package/src/rbac/role.manager.ts +75 -0
- package/src/security/csrf/csrf.protection.ts +37 -0
- package/src/security/index.ts +3 -0
- package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
- package/src/security/rate-limiting/auth.module.ts +32 -0
- package/src/server/auth.module.ts +158 -0
- package/src/server/decorators/auth.decorator.ts +43 -0
- package/src/server/decorators/auth.decorators.ts +31 -0
- package/src/server/decorators/current-user.decorator.ts +49 -0
- package/src/server/decorators/permission.decorator.ts +49 -0
- package/src/server/guards/auth.guard.ts +56 -0
- package/src/server/guards/custom-throttler.guard.ts +46 -0
- package/src/server/guards/permissions.guard.ts +115 -0
- package/src/server/guards/roles.guard.ts +31 -0
- package/src/server/middleware/auth.middleware.ts +46 -0
- package/src/server/middleware/index.ts +2 -0
- package/src/server/middleware/middleware.ts +11 -0
- package/src/server/middleware/session.middleware.ts +255 -0
- package/src/server/services/account.service.ts +269 -0
- package/src/server/services/auth.service.ts +79 -0
- package/src/server/services/brute-force.service.ts +98 -0
- package/src/server/services/index.ts +15 -0
- package/src/server/services/rate-limiter.service.ts +60 -0
- package/src/server/services/session.service.ts +287 -0
- package/src/server/services/token.service.ts +262 -0
- package/src/session/cookie-store.ts +255 -0
- package/src/session/enhanced-session-manager.ts +406 -0
- package/src/session/index.ts +14 -0
- package/src/session/memory-store.ts +320 -0
- package/src/session/redis-store.ts +443 -0
- package/src/strategies/oauth.strategy.ts +128 -0
- package/src/strategies/traditional-auth.strategy.ts +116 -0
- package/src/tokens/index.ts +4 -0
- package/src/tokens/refresh-token-manager.ts +448 -0
- package/src/tokens/token-validator.ts +311 -0
- package/tsconfig.build.json +28 -0
- package/tsconfig.json +38 -0
- package/tsup.config.mjs +28 -0
- package/vitest.config.mjs +16 -0
- package/vitest.setup.d.ts +2 -0
- package/vitest.setup.d.ts.map +1 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { NUMERIX } from "@plyaz/config";
|
|
2
|
+
|
|
3
|
+
export interface RouteRateLimitConfig {
|
|
4
|
+
limit: number; // max requests
|
|
5
|
+
windowMs: number; // time window
|
|
6
|
+
blockMs?: number; // block duration after limit exceeded
|
|
7
|
+
}
|
|
8
|
+
const thirty = 30;
|
|
9
|
+
const fifteen = 15;
|
|
10
|
+
export const TIME = {
|
|
11
|
+
SECOND: 1000,
|
|
12
|
+
MINUTE: 60_000,
|
|
13
|
+
FIVE_MIN: NUMERIX.FIVE * NUMERIX.SIXTY_THOUSAND,
|
|
14
|
+
FIFTEEN_MIN: fifteen * NUMERIX.SIXTY_THOUSAND,
|
|
15
|
+
THIRTY_MIN: thirty * NUMERIX.SIXTY_THOUSAND,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export const authRateLimits = {
|
|
19
|
+
signin: {
|
|
20
|
+
limit: 10,
|
|
21
|
+
windowMs: TIME.MINUTE,
|
|
22
|
+
blockMs: TIME.FIFTEEN_MIN,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
signup: {
|
|
26
|
+
limit: 5,
|
|
27
|
+
windowMs: TIME.MINUTE,
|
|
28
|
+
blockMs: TIME.THIRTY_MIN,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
refresh: {
|
|
32
|
+
limit: 30,
|
|
33
|
+
windowMs: TIME.MINUTE,
|
|
34
|
+
blockMs: TIME.FIVE_MIN,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
mfaVerify: {
|
|
38
|
+
limit: 5,
|
|
39
|
+
windowMs: TIME.MINUTE,
|
|
40
|
+
blockMs: TIME.THIRTY_MIN,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
web3Nonce: {
|
|
44
|
+
limit: 20,
|
|
45
|
+
windowMs: TIME.MINUTE,
|
|
46
|
+
blockMs: TIME.FIVE_MIN,
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
web3Verify: {
|
|
50
|
+
limit: 10,
|
|
51
|
+
windowMs: TIME.MINUTE,
|
|
52
|
+
blockMs: TIME.FIFTEEN_MIN,
|
|
53
|
+
},
|
|
54
|
+
} as const satisfies Record<string, RouteRateLimitConfig>;
|
|
55
|
+
|
|
56
|
+
export const rateLimitConfig: Record<string, RouteRateLimitConfig> = {
|
|
57
|
+
"POST /auth/signin": { limit: 10, windowMs: 60_000, blockMs: fifteen * NUMERIX.SIXTY_THOUSAND },
|
|
58
|
+
"POST /auth/signup": { limit: 5, windowMs: 60_000, blockMs: thirty * NUMERIX.SIXTY_THOUSAND },
|
|
59
|
+
"POST /auth/refresh": { limit: 30, windowMs: 60_000, blockMs: NUMERIX.FIVE * NUMERIX.SIXTY_THOUSAND },
|
|
60
|
+
"POST /auth/mfa/verify": { limit: 5, windowMs: 60_000, blockMs: thirty * NUMERIX.SIXTY_THOUSAND },
|
|
61
|
+
"GET /auth/web3/nonce": { limit: 20, windowMs: 60_000, blockMs: NUMERIX.FIVE * NUMERIX.SIXTY_THOUSAND },
|
|
62
|
+
"POST /auth/web3/verify": {
|
|
63
|
+
limit: 10,
|
|
64
|
+
windowMs: 60_000,
|
|
65
|
+
blockMs: fifteen * NUMERIX.SIXTY_THOUSAND,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { ThrottlerModule } from "@nestjs/throttler";
|
|
3
|
+
import { Redis } from "ioredis";
|
|
4
|
+
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
|
5
|
+
import { AuthController } from "./auth/auth.controller";
|
|
6
|
+
import { RateLimiterGuard } from "../../server/guards/custom-throttler.guard";
|
|
7
|
+
import { BruteForceService, RateLimiterService } from "../../server/services";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
ThrottlerModule.forRootAsync({
|
|
13
|
+
useFactory: () => ({
|
|
14
|
+
storage: new ThrottlerStorageRedisService(
|
|
15
|
+
new Redis({
|
|
16
|
+
host: "localhost",
|
|
17
|
+
port: 6379,
|
|
18
|
+
})
|
|
19
|
+
),
|
|
20
|
+
throttlers: [
|
|
21
|
+
{
|
|
22
|
+
ttl: 60000,
|
|
23
|
+
limit: 10,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
controllers: [AuthController],
|
|
30
|
+
providers: [RateLimiterGuard, RateLimiterService, BruteForceService],
|
|
31
|
+
})
|
|
32
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview NestJS authentication module for @plyaz/auth
|
|
3
|
+
* @module @plyaz/auth/server/auth-module
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Main NestJS module that provides all authentication and authorization services,
|
|
7
|
+
* guards, decorators, and middleware. Configures dependency injection for the
|
|
8
|
+
* entire auth system including session management, token validation, RBAC,
|
|
9
|
+
* and security features.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { AuthModule } from '@plyaz/auth';
|
|
14
|
+
*
|
|
15
|
+
* @Module({
|
|
16
|
+
* imports: [AuthModule],
|
|
17
|
+
* })
|
|
18
|
+
* export class AppModule {}
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Module } from '@nestjs/common';
|
|
23
|
+
import { ConfigModule } from '@nestjs/config';
|
|
24
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
25
|
+
|
|
26
|
+
// Services
|
|
27
|
+
import { AuthService } from './services/auth.service';
|
|
28
|
+
import { SessionService } from './services/session.service';
|
|
29
|
+
import { TokenService } from './services/token.service';
|
|
30
|
+
import { AccountService } from './services/account.service';
|
|
31
|
+
|
|
32
|
+
// Guards
|
|
33
|
+
import { AuthGuard } from './guards/auth.guard';
|
|
34
|
+
import { RolesGuard } from './guards/roles.guard';
|
|
35
|
+
import { PermissionsGuard } from './guards/permissions.guard';
|
|
36
|
+
|
|
37
|
+
// Core Components
|
|
38
|
+
import { SessionManager } from '../core/session/session.manager';
|
|
39
|
+
import { EnhancedSessionManager } from '../session/enhanced-session-manager';
|
|
40
|
+
import { RoleManager } from '../rbac/role.manager';
|
|
41
|
+
import { PermissionChecker } from '../rbac/permission-checker';
|
|
42
|
+
|
|
43
|
+
// Strategies
|
|
44
|
+
import { TraditionalAuthStrategy } from '../strategies/traditional-auth.strategy';
|
|
45
|
+
import { OAuthStrategy } from '../strategies/oauth.strategy';
|
|
46
|
+
|
|
47
|
+
// Token Management
|
|
48
|
+
import { TokenValidator } from '../tokens/token-validator';
|
|
49
|
+
import { RefreshTokenManager } from '../tokens/refresh-token-manager';
|
|
50
|
+
import { TokenBlacklist } from '../core/blacklist/token.blacklist';
|
|
51
|
+
|
|
52
|
+
// Security
|
|
53
|
+
|
|
54
|
+
import { CSRFProtection } from '../security/csrf/csrf.protection';
|
|
55
|
+
|
|
56
|
+
// Repositories
|
|
57
|
+
import { UserRepository } from '../db/repositories/user.repository';
|
|
58
|
+
import { SessionRepository } from '../db/repositories/session.repository';
|
|
59
|
+
import { ConnectedAccountRepository } from '../db/repositories/connected-account.repository';
|
|
60
|
+
|
|
61
|
+
// Session Stores
|
|
62
|
+
import { MemoryStore } from '../session/memory-store';
|
|
63
|
+
import { RateLimiterService } from './services';
|
|
64
|
+
|
|
65
|
+
@Module({
|
|
66
|
+
imports: [
|
|
67
|
+
ConfigModule,
|
|
68
|
+
JwtModule.register({
|
|
69
|
+
secret: globalThis.process.env.JWT_SECRET ?? 'default-secret',
|
|
70
|
+
signOptions: { expiresIn: '15m' },
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
providers: [
|
|
74
|
+
// Services
|
|
75
|
+
AuthService,
|
|
76
|
+
SessionService,
|
|
77
|
+
TokenService,
|
|
78
|
+
AccountService,
|
|
79
|
+
|
|
80
|
+
// Guards
|
|
81
|
+
AuthGuard,
|
|
82
|
+
RolesGuard,
|
|
83
|
+
PermissionsGuard,
|
|
84
|
+
|
|
85
|
+
// Core Components
|
|
86
|
+
SessionManager,
|
|
87
|
+
EnhancedSessionManager,
|
|
88
|
+
RoleManager,
|
|
89
|
+
PermissionChecker,
|
|
90
|
+
|
|
91
|
+
// Strategies
|
|
92
|
+
TraditionalAuthStrategy,
|
|
93
|
+
OAuthStrategy,
|
|
94
|
+
|
|
95
|
+
// Token Management
|
|
96
|
+
TokenValidator,
|
|
97
|
+
RefreshTokenManager,
|
|
98
|
+
TokenBlacklist,
|
|
99
|
+
|
|
100
|
+
// Security
|
|
101
|
+
RateLimiterService,
|
|
102
|
+
CSRFProtection,
|
|
103
|
+
|
|
104
|
+
// Repositories
|
|
105
|
+
UserRepository,
|
|
106
|
+
SessionRepository,
|
|
107
|
+
ConnectedAccountRepository,
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// Session Store
|
|
111
|
+
{
|
|
112
|
+
provide: 'SessionStore',
|
|
113
|
+
useClass: MemoryStore,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Configuration Providers
|
|
117
|
+
{
|
|
118
|
+
provide: 'JWT_CONFIG',
|
|
119
|
+
useValue: {
|
|
120
|
+
privateKey: globalThis.process.env.JWT_PRIVATE_KEY ?? 'default-private-key',
|
|
121
|
+
publicKey: globalThis.process.env.JWT_PUBLIC_KEY ?? 'default-public-key',
|
|
122
|
+
issuer: globalThis.process.env.JWT_ISSUER ?? 'plyaz.com',
|
|
123
|
+
audience: globalThis.process.env.JWT_AUDIENCE ?? 'plyaz-api',
|
|
124
|
+
accessTokenTTL: globalThis.parseInt(globalThis.process.env.ACCESS_TOKEN_TTL ?? '900'), // 15 minutes
|
|
125
|
+
refreshTokenTTL:globalThis.parseInt(globalThis.process.env.REFRESH_TOKEN_TTL ?? '604800'), // 7 days
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
exports: [
|
|
130
|
+
// Services
|
|
131
|
+
AuthService,
|
|
132
|
+
SessionService,
|
|
133
|
+
TokenService,
|
|
134
|
+
AccountService,
|
|
135
|
+
|
|
136
|
+
// Guards
|
|
137
|
+
AuthGuard,
|
|
138
|
+
RolesGuard,
|
|
139
|
+
PermissionsGuard,
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
SessionManager,
|
|
143
|
+
EnhancedSessionManager,
|
|
144
|
+
RoleManager,
|
|
145
|
+
PermissionChecker,
|
|
146
|
+
|
|
147
|
+
// Token Management
|
|
148
|
+
TokenValidator,
|
|
149
|
+
RefreshTokenManager,
|
|
150
|
+
|
|
151
|
+
// Repositories
|
|
152
|
+
UserRepository,
|
|
153
|
+
SessionRepository,
|
|
154
|
+
ConnectedAccountRepository,
|
|
155
|
+
|
|
156
|
+
],
|
|
157
|
+
})
|
|
158
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @Auth() and @Public() decorators
|
|
3
|
+
* @module @plyaz/auth/server/decorators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';
|
|
7
|
+
import { AuthGuard } from '../guards/auth.guard';
|
|
8
|
+
|
|
9
|
+
export const IS_PUBLIC_KEY = 'isPublic';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Decorator to require authentication for endpoint
|
|
13
|
+
* Combines @UseGuards(AuthGuard) for convenience
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* @Get('protected')
|
|
18
|
+
* @Auth()
|
|
19
|
+
* getProtectedData(@CurrentUser() user: User) {
|
|
20
|
+
* return { message: 'This is protected data' };
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const Auth = (): MethodDecorator & ClassDecorator =>
|
|
25
|
+
applyDecorators(
|
|
26
|
+
UseGuards(AuthGuard),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decorator to mark endpoint as public (skip authentication)
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* @Get('health')
|
|
35
|
+
* @Public()
|
|
36
|
+
* getHealth() {
|
|
37
|
+
* return { status: 'ok' };
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export const Public = (): MethodDecorator & ClassDecorator =>
|
|
43
|
+
SetMetadata(IS_PUBLIC_KEY, true);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { SetMetadata, createParamDecorator } from '@nestjs/common';
|
|
3
|
+
|
|
4
|
+
// Public decorator
|
|
5
|
+
export const Public = (): MethodDecorator & ClassDecorator =>
|
|
6
|
+
SetMetadata('isPublic', true);
|
|
7
|
+
|
|
8
|
+
// Roles decorator
|
|
9
|
+
export const Roles = (...roles: string[]): MethodDecorator & ClassDecorator =>
|
|
10
|
+
SetMetadata('roles', roles);
|
|
11
|
+
|
|
12
|
+
// Permissions decorator
|
|
13
|
+
export const Permissions = (...permissions: string[]): MethodDecorator & ClassDecorator =>
|
|
14
|
+
SetMetadata('permissions', permissions);
|
|
15
|
+
export const CurrentUser = createParamDecorator(
|
|
16
|
+
(data: unknown, ctx: ExecutionContext) => {
|
|
17
|
+
const request = ctx.switchToHttp().getRequest();
|
|
18
|
+
return request.user;
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const CurrentSession = createParamDecorator(
|
|
23
|
+
(data: unknown, ctx: ExecutionContext) => {
|
|
24
|
+
const request = ctx.switchToHttp().getRequest();
|
|
25
|
+
return request.session;
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const Auth = ():void => {
|
|
30
|
+
|
|
31
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @CurrentUser() parameter decorator
|
|
3
|
+
* @module @plyaz/auth/server/decorators
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ExecutionContext } from '@nestjs/common';
|
|
7
|
+
import { createParamDecorator } from '@nestjs/common';
|
|
8
|
+
import type { User } from '@/common/types/auth.types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parameter decorator to inject authenticated user into controller methods
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* @Get('profile')
|
|
16
|
+
* @Auth()
|
|
17
|
+
* getProfile(@CurrentUser() user: User) {
|
|
18
|
+
* return { id: user.id, email: user.email };
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const CurrentUser = createParamDecorator(
|
|
23
|
+
(data: keyof User | undefined, ctx: ExecutionContext): User => {
|
|
24
|
+
const request = ctx.switchToHttp().getRequest();
|
|
25
|
+
const user = request.user;
|
|
26
|
+
|
|
27
|
+
// Return specific property if requested
|
|
28
|
+
return data ? user?.[data] : user;
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get access token from request
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* @Post('logout')
|
|
38
|
+
* @Auth()
|
|
39
|
+
* async logout(@CurrentToken() token: string) {
|
|
40
|
+
* await this.authService.signOut(token);
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const CurrentToken = createParamDecorator(
|
|
45
|
+
(data: unknown, ctx: ExecutionContext): string => {
|
|
46
|
+
const request = ctx.switchToHttp().getRequest();
|
|
47
|
+
return request.accessToken;
|
|
48
|
+
},
|
|
49
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Permission decorator metadata key
|
|
5
|
+
*/
|
|
6
|
+
export const PERMISSION_KEY = 'permission';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Permission decorator for setting required permissions
|
|
10
|
+
* @param resource - Resource name (e.g., 'campaigns', 'users', 'organizations')
|
|
11
|
+
* @param action - Action name (e.g., 'create', 'read', 'update', 'delete')
|
|
12
|
+
* @param conditions - Optional conditions for the permission
|
|
13
|
+
* @returns Method & Class decorator
|
|
14
|
+
*/
|
|
15
|
+
export const Permission = (
|
|
16
|
+
resource: string,
|
|
17
|
+
action: string,
|
|
18
|
+
conditions?: Record<string, string>
|
|
19
|
+
): MethodDecorator & ClassDecorator => {
|
|
20
|
+
const permissionData = conditions
|
|
21
|
+
? { resource, action, conditions }
|
|
22
|
+
: [resource, action];
|
|
23
|
+
|
|
24
|
+
return SetMetadata(PERMISSION_KEY, permissionData);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Permissions decorator for setting multiple required permissions
|
|
29
|
+
* User must have ALL specified permissions
|
|
30
|
+
* @param permissions - Array of permission objects
|
|
31
|
+
* @returns Method & Class decorator
|
|
32
|
+
*/
|
|
33
|
+
export const Permissions = (
|
|
34
|
+
permissions: Array<{ resource: string; action: string; conditions?: Record<string, string> }>
|
|
35
|
+
): MethodDecorator & ClassDecorator => {
|
|
36
|
+
return SetMetadata('permissions', permissions);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* AnyPermission decorator for setting alternative permissions
|
|
41
|
+
* User must have ANY of the specified permissions
|
|
42
|
+
* @param permissions - Array of permission objects
|
|
43
|
+
* @returns Method & Class decorator
|
|
44
|
+
*/
|
|
45
|
+
export const AnyPermission = (
|
|
46
|
+
permissions: Array<{ resource: string; action: string; conditions?: Record<string, string> }>
|
|
47
|
+
): MethodDecorator & ClassDecorator => {
|
|
48
|
+
return SetMetadata('anyPermission', permissions);
|
|
49
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import type { Reflector } from '@nestjs/core';
|
|
4
|
+
|
|
5
|
+
import type { SessionManager } from '../../core/session/session.manager';
|
|
6
|
+
import { AuthenticationError } from '@plyaz/errors';
|
|
7
|
+
import type { JwtManager } from '@/core';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class AuthGuard implements CanActivate {
|
|
11
|
+
constructor(
|
|
12
|
+
private jwtManager: JwtManager,
|
|
13
|
+
private sessionManager: SessionManager,
|
|
14
|
+
private reflector: Reflector
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
// Explicit return type for canActivate
|
|
18
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
19
|
+
const isPublic: boolean | undefined = this.reflector.get<boolean>('isPublic', context.getHandler());
|
|
20
|
+
if (isPublic) return true;
|
|
21
|
+
|
|
22
|
+
const request = context.switchToHttp().getRequest();
|
|
23
|
+
const token: string | null = this.extractToken(request);
|
|
24
|
+
if (!token) throw new AuthenticationError('AUTH_TOKEN_INVALID');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const payload = this.jwtManager.verifyAccessToken(token);
|
|
28
|
+
const session = await this.sessionManager.getSession(payload.sessionId);
|
|
29
|
+
|
|
30
|
+
if (!session) throw new AuthenticationError('AUTH_SESSION_EXPIRED');
|
|
31
|
+
|
|
32
|
+
await this.sessionManager.updateLastActive(session.id);
|
|
33
|
+
|
|
34
|
+
// Attach user and session to request
|
|
35
|
+
request.user = payload;
|
|
36
|
+
request.session = session;
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
} catch (error: unknown) {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
if (error.name === 'TokenExpiredError') {
|
|
42
|
+
throw new AuthenticationError('AUTH_TOKEN_EXPIRED');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new AuthenticationError('AUTH_TOKEN_INVALID');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Explicit return type for private helper
|
|
50
|
+
private extractToken(request: { headers: { authorization?: string } }): string | null {
|
|
51
|
+
const authHeader = request.headers.authorization;
|
|
52
|
+
if (!authHeader?.startsWith('Bearer ')) return null;
|
|
53
|
+
const seven = 7;
|
|
54
|
+
return authHeader.substring(seven);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext} from "@nestjs/common";
|
|
4
|
+
import {
|
|
5
|
+
Injectable,
|
|
6
|
+
BadRequestException,
|
|
7
|
+
} from "@nestjs/common";
|
|
8
|
+
import type { RateLimiterService } from "../services/rate-limiter.service";
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class RateLimiterGuard implements CanActivate {
|
|
12
|
+
constructor(private readonly rateLimiterService: RateLimiterService) {}
|
|
13
|
+
|
|
14
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
15
|
+
const req = context.switchToHttp().getRequest();
|
|
16
|
+
|
|
17
|
+
// Get IP address from headers or socket
|
|
18
|
+
const ip =
|
|
19
|
+
req.headers["x-forwarded-for"]?.split(",")[0] ??
|
|
20
|
+
req.ip ??
|
|
21
|
+
req.socket?.remoteAddress;
|
|
22
|
+
const identifier = `${ip}`;
|
|
23
|
+
|
|
24
|
+
// Construct route key, e.g., "POST /auth/signin"
|
|
25
|
+
const routeKey = `${req.method} ${req.route.path}`;
|
|
26
|
+
|
|
27
|
+
// Get rate limit options for this route
|
|
28
|
+
const options = this.rateLimiterService.getOption(routeKey);
|
|
29
|
+
|
|
30
|
+
// If no rate limit configured, allow request
|
|
31
|
+
if (options === undefined) return true;
|
|
32
|
+
|
|
33
|
+
// Check if the request is rate limited
|
|
34
|
+
const isLimited = await this.rateLimiterService.isRateLimited(
|
|
35
|
+
identifier,
|
|
36
|
+
options
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (isLimited)
|
|
40
|
+
throw new BadRequestException(
|
|
41
|
+
"Too many requests, please try again later"
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Injectable, type CanActivate, type ExecutionContext } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import type { Reflector } from '@nestjs/core';
|
|
4
|
+
import type { PermissionChecker } from '../../rbac/permission-checker';
|
|
5
|
+
import { AuthenticationError } from '@plyaz/errors';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Typed request interface including user and request data
|
|
9
|
+
*/
|
|
10
|
+
interface TypedRequest {
|
|
11
|
+
user?: {
|
|
12
|
+
sub?: string;
|
|
13
|
+
userId?: string;
|
|
14
|
+
};
|
|
15
|
+
params?: Record<string, string>;
|
|
16
|
+
query?: Record<string, string>;
|
|
17
|
+
body?: Record<string, string>;
|
|
18
|
+
permissionResult?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Permission metadata interface
|
|
23
|
+
*/
|
|
24
|
+
interface PermissionMetadata {
|
|
25
|
+
resource: string;
|
|
26
|
+
action: string;
|
|
27
|
+
conditions?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Injectable()
|
|
31
|
+
export class PermissionsGuard implements CanActivate {
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly reflector: Reflector,
|
|
34
|
+
private readonly permissionChecker: PermissionChecker
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
38
|
+
const request: TypedRequest = context.switchToHttp().getRequest();
|
|
39
|
+
const permissionMetadata = this.getPermissionMetadata(context);
|
|
40
|
+
|
|
41
|
+
if (!permissionMetadata) return true; // no permission required
|
|
42
|
+
|
|
43
|
+
const user = request.user;
|
|
44
|
+
if (!user) {
|
|
45
|
+
throw new AuthenticationError(
|
|
46
|
+
'AUTH_INVALID_CREDENTIALS'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const permissionContext = this.buildPermissionContext(request, permissionMetadata);
|
|
51
|
+
|
|
52
|
+
const result = await this.permissionChecker.checkPermission(
|
|
53
|
+
user.sub ?? user.userId ?? '',
|
|
54
|
+
permissionMetadata.resource,
|
|
55
|
+
permissionMetadata.action,
|
|
56
|
+
permissionContext
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!result.granted) {
|
|
60
|
+
throw new AuthenticationError(
|
|
61
|
+
'AUTH_INSUFFICIENT_PERMISSIONS'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
request.permissionResult = result; // attach for handler usage
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private getPermissionMetadata(context: ExecutionContext): PermissionMetadata | null {
|
|
71
|
+
const permissionData = this.reflector.get<[string, string] | PermissionMetadata>(
|
|
72
|
+
'permission',
|
|
73
|
+
context.getHandler()
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!permissionData) return null;
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(permissionData)) {
|
|
79
|
+
return { resource: permissionData[0], action: permissionData[1] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return permissionData;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private buildPermissionContext(
|
|
86
|
+
request: TypedRequest,
|
|
87
|
+
metadata: PermissionMetadata
|
|
88
|
+
): Record<string, string> {
|
|
89
|
+
const context: Record<string, string> = {};
|
|
90
|
+
|
|
91
|
+
if (request.user) {
|
|
92
|
+
context.ownerId = request.user.sub ?? request.user.userId ?? '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (request.params) Object.assign(context, request.params);
|
|
96
|
+
|
|
97
|
+
if (request.query) {
|
|
98
|
+
const relevantQueryParams = ['organizationId', 'teamId', 'projectId'];
|
|
99
|
+
for (const param of relevantQueryParams) {
|
|
100
|
+
if (request.query[param]) context[param] = request.query[param];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (request.body) {
|
|
105
|
+
const relevantBodyFields = ['ownerId', 'organizationId', 'teamId', 'projectId'];
|
|
106
|
+
for (const field of relevantBodyFields) {
|
|
107
|
+
if (request.body[field]) context[field] = request.body[field];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (metadata.conditions) Object.assign(context, metadata.conditions);
|
|
112
|
+
|
|
113
|
+
return context;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import type { Reflector } from '@nestjs/core';
|
|
4
|
+
import type { RoleManager } from '../../rbac/role.manager';
|
|
5
|
+
import { AuthenticationError } from '@plyaz/errors';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class RolesGuard implements CanActivate {
|
|
10
|
+
constructor(
|
|
11
|
+
private roleManager: RoleManager,
|
|
12
|
+
private reflector: Reflector
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
16
|
+
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
|
|
17
|
+
if (!requiredRoles) return true;
|
|
18
|
+
|
|
19
|
+
const request = context.switchToHttp().getRequest();
|
|
20
|
+
const user = request.user;
|
|
21
|
+
|
|
22
|
+
if (!user) throw new AuthenticationError('AUTH_INVALID_CREDENTIALS');
|
|
23
|
+
|
|
24
|
+
const hasRole = await this.roleManager.hasAnyRole(user.sub, requiredRoles);
|
|
25
|
+
if (!hasRole) {
|
|
26
|
+
throw new AuthenticationError('AUTH_ROLE_REQUIRED');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|