saas-backend-kit 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.
Files changed (67) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/PUBLISHING.md +133 -0
  3. package/README.md +459 -0
  4. package/copy-dts.js +255 -0
  5. package/dist/auth/index.d.ts +58 -0
  6. package/dist/auth/index.js +584 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/auth/index.mjs +569 -0
  9. package/dist/auth/index.mjs.map +1 -0
  10. package/dist/config/index.d.ts +22 -0
  11. package/dist/config/index.js +106 -0
  12. package/dist/config/index.js.map +1 -0
  13. package/dist/config/index.mjs +100 -0
  14. package/dist/config/index.mjs.map +1 -0
  15. package/dist/index.d.ts +25 -0
  16. package/dist/index.js +1303 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +1281 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/dist/logger/index.d.ts +18 -0
  21. package/dist/logger/index.js +188 -0
  22. package/dist/logger/index.js.map +1 -0
  23. package/dist/logger/index.mjs +178 -0
  24. package/dist/logger/index.mjs.map +1 -0
  25. package/dist/notifications/index.d.ts +35 -0
  26. package/dist/notifications/index.js +339 -0
  27. package/dist/notifications/index.js.map +1 -0
  28. package/dist/notifications/index.mjs +328 -0
  29. package/dist/notifications/index.mjs.map +1 -0
  30. package/dist/queue/index.d.ts +33 -0
  31. package/dist/queue/index.js +306 -0
  32. package/dist/queue/index.js.map +1 -0
  33. package/dist/queue/index.mjs +293 -0
  34. package/dist/queue/index.mjs.map +1 -0
  35. package/dist/rate-limit/index.d.ts +11 -0
  36. package/dist/rate-limit/index.js +290 -0
  37. package/dist/rate-limit/index.js.map +1 -0
  38. package/dist/rate-limit/index.mjs +286 -0
  39. package/dist/rate-limit/index.mjs.map +1 -0
  40. package/dist/response/index.d.ts +29 -0
  41. package/dist/response/index.js +120 -0
  42. package/dist/response/index.js.map +1 -0
  43. package/dist/response/index.mjs +114 -0
  44. package/dist/response/index.mjs.map +1 -0
  45. package/examples/express/.env.example +41 -0
  46. package/examples/express/app.ts +203 -0
  47. package/package.json +109 -0
  48. package/src/auth/express.ts +250 -0
  49. package/src/auth/fastify.ts +65 -0
  50. package/src/auth/index.ts +6 -0
  51. package/src/auth/jwt.ts +47 -0
  52. package/src/auth/oauth.ts +117 -0
  53. package/src/auth/rbac.ts +82 -0
  54. package/src/auth/types.ts +69 -0
  55. package/src/config/index.ts +120 -0
  56. package/src/index.ts +16 -0
  57. package/src/logger/index.ts +110 -0
  58. package/src/notifications/index.ts +262 -0
  59. package/src/plugin.ts +192 -0
  60. package/src/queue/index.ts +208 -0
  61. package/src/rate-limit/express.ts +144 -0
  62. package/src/rate-limit/fastify.ts +47 -0
  63. package/src/rate-limit/index.ts +2 -0
  64. package/src/response/index.ts +197 -0
  65. package/src/utils/index.ts +180 -0
  66. package/tsconfig.json +30 -0
  67. package/tsup.config.ts +24 -0
@@ -0,0 +1,41 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+
4
+ # JWT
5
+ JWT_SECRET=your-super-secret-jwt-key-change-in-production-min-32-chars
6
+ JWT_EXPIRES_IN=7d
7
+ JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-in-production
8
+ JWT_REFRESH_EXPIRES_IN=30d
9
+
10
+ # Redis
11
+ REDIS_URL=redis://localhost:6379
12
+
13
+ # Database (optional)
14
+ DATABASE_URL=postgresql://user:password@localhost:5432/mydb
15
+
16
+ # Google OAuth (optional)
17
+ GOOGLE_CLIENT_ID=
18
+ GOOGLE_CLIENT_SECRET=
19
+ GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
20
+
21
+ # Email (SMTP) (optional)
22
+ SMTP_HOST=smtp.example.com
23
+ SMTP_PORT=587
24
+ SMTP_USER=your-smtp-user
25
+ SMTP_PASS=your-smtp-password
26
+ SMTP_FROM=noreply@yourdomain.com
27
+
28
+ # Twilio SMS (optional)
29
+ TWILIO_ACCOUNT_SID=
30
+ TWILIO_AUTH_TOKEN=
31
+ TWILIO_PHONE_NUMBER=
32
+
33
+ # Slack (optional)
34
+ SLACK_WEBHOOK_URL=
35
+
36
+ # Rate Limiting
37
+ RATE_LIMIT_WINDOW=1m
38
+ RATE_LIMIT_LIMIT=100
39
+
40
+ # Logger
41
+ LOG_LEVEL=info
@@ -0,0 +1,203 @@
1
+ import express from 'express';
2
+ import {
3
+ auth,
4
+ rateLimit,
5
+ logger,
6
+ config,
7
+ queue,
8
+ notify,
9
+ response,
10
+ createApp
11
+ } from '../src';
12
+
13
+ config.load();
14
+
15
+ const app = express();
16
+
17
+ app.use(express.json());
18
+
19
+ const authMiddleware = auth.initialize({
20
+ jwtSecret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
21
+ jwtExpiresIn: '7d',
22
+ refreshExpiresIn: '30d',
23
+ googleClientId: process.env.GOOGLE_CLIENT_ID,
24
+ googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
25
+ googleRedirectUri: process.env.GOOGLE_REDIRECT_URI,
26
+ });
27
+
28
+ app.use(authMiddleware);
29
+
30
+ app.use(rateLimit({
31
+ window: '1m',
32
+ limit: 100,
33
+ }));
34
+
35
+ app.post('/auth/register', async (req, res) => {
36
+ try {
37
+ const { email, password, name } = req.body;
38
+ const result = await auth().register({ email, password, name });
39
+ res.success(result.tokens, 'Registration successful');
40
+ } catch (error: any) {
41
+ res.badRequest(error.message);
42
+ }
43
+ });
44
+
45
+ app.post('/auth/login', async (req, res) => {
46
+ try {
47
+ const { email, password } = req.body;
48
+ const result = await auth().login({ email, password });
49
+ res.success(result.tokens, 'Login successful');
50
+ } catch (error: any) {
51
+ res.unauthorized('Invalid credentials');
52
+ }
53
+ });
54
+
55
+ app.post('/auth/refresh', async (req, res) => {
56
+ try {
57
+ const { refreshToken } = req.body;
58
+ const tokens = await auth().refresh(refreshToken);
59
+ res.success(tokens);
60
+ } catch (error: any) {
61
+ res.unauthorized('Invalid refresh token');
62
+ }
63
+ });
64
+
65
+ app.get('/auth/google', async (req, res) => {
66
+ const url = await auth().getGoogleAuthUrl();
67
+ res.redirect(url);
68
+ });
69
+
70
+ app.get('/auth/google/callback', async (req, res) => {
71
+ try {
72
+ const { code } = req.query;
73
+ const result = await auth().handleGoogleCallback(code as string);
74
+ res.success(result.tokens, 'Google login successful');
75
+ } catch (error: any) {
76
+ res.badRequest('Google authentication failed');
77
+ }
78
+ });
79
+
80
+ app.get('/dashboard', auth.requireUser(), (req, res) => {
81
+ res.success({ message: 'Welcome to your dashboard', user: req.user });
82
+ });
83
+
84
+ app.get('/admin', auth.requireRole('admin'), (req, res) => {
85
+ res.success({ message: 'Admin area' });
86
+ });
87
+
88
+ app.get('/protected', auth.requirePermission('read'), (req, res) => {
89
+ res.success({ message: 'You have read permission' });
90
+ });
91
+
92
+ const emailQueue = queue.create('email', {
93
+ defaultJobOptions: {
94
+ attempts: 3,
95
+ backoff: {
96
+ type: 'exponential',
97
+ delay: 1000,
98
+ },
99
+ },
100
+ });
101
+
102
+ emailQueue.add('sendWelcomeEmail', { userId: '123', email: 'user@example.com' }, {
103
+ delay: 5000,
104
+ });
105
+
106
+ queue.process('email', async (job) => {
107
+ logger.info(`Processing email job`, { jobId: job.id, type: job.name });
108
+
109
+ await notify.email({
110
+ to: job.data.email,
111
+ subject: 'Welcome!',
112
+ template: 'welcome',
113
+ templateData: { name: 'User', appName: 'My SaaS App' },
114
+ });
115
+
116
+ return { sent: true };
117
+ });
118
+
119
+ app.post('/notify/email', async (req, res) => {
120
+ try {
121
+ const { to, subject, template, data } = req.body;
122
+ await notify.email({ to, subject, template, templateData: data });
123
+ res.success({ message: 'Email sent' });
124
+ } catch (error: any) {
125
+ res.internalError(error.message);
126
+ }
127
+ });
128
+
129
+ app.post('/notify/sms', async (req, res) => {
130
+ try {
131
+ const { to, message } = req.body;
132
+ await notify.sms({ to, message });
133
+ res.success({ message: 'SMS sent' });
134
+ } catch (error: any) {
135
+ res.internalError(error.message);
136
+ }
137
+ });
138
+
139
+ app.post('/notify/webhook', async (req, res) => {
140
+ try {
141
+ const { url, method, body, headers } = req.body;
142
+ const result = await notify.webhook({ url, method, body, headers });
143
+ res.success(result);
144
+ } catch (error: any) {
145
+ res.internalError(error.message);
146
+ }
147
+ });
148
+
149
+ app.post('/notify/slack', async (req, res) => {
150
+ try {
151
+ const { text, blocks } = req.body;
152
+ await notify.slack({ text, blocks });
153
+ res.success({ message: 'Slack notification sent' });
154
+ } catch (error: any) {
155
+ res.internalError(error.message);
156
+ }
157
+ });
158
+
159
+ app.get('/api/users', (req, res) => {
160
+ const users = [
161
+ { id: '1', name: 'John Doe', email: 'john@example.com' },
162
+ { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
163
+ ];
164
+
165
+ const page = parseInt(req.query.page as string) || 1;
166
+ const limit = parseInt(req.query.limit as string) || 10;
167
+ const total = users.length;
168
+
169
+ res.paginated(users, page, limit, total);
170
+ });
171
+
172
+ app.get('/api/users/:id', (req, res) => {
173
+ const user = { id: req.params.id, name: 'John Doe', email: 'john@example.com' };
174
+ res.success(user);
175
+ });
176
+
177
+ app.post('/api/users', (req, res) => {
178
+ const user = { id: '3', ...req.body };
179
+ res.created(user, 'User created');
180
+ });
181
+
182
+ app.put('/api/users/:id', (req, res) => {
183
+ const user = { id: req.params.id, ...req.body };
184
+ res.updated(user, 'User updated');
185
+ });
186
+
187
+ app.delete('/api/users/:id', (req, res) => {
188
+ res.deleted('User deleted');
189
+ });
190
+
191
+ const PORT = config.int('PORT') || 3000;
192
+
193
+ app.listen(PORT, () => {
194
+ logger.info(`Server running on port ${PORT}`);
195
+ });
196
+
197
+ process.on('SIGTERM', async () => {
198
+ logger.info('SIGTERM received, closing server');
199
+ await queue.closeAll();
200
+ process.exit(0);
201
+ });
202
+
203
+ export default app;
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "saas-backend-kit",
3
+ "version": "1.0.0",
4
+ "description": "Production-grade modular backend toolkit for building scalable SaaS applications",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./auth": {
15
+ "types": "./dist/auth/index.d.ts",
16
+ "import": "./dist/auth/index.mjs",
17
+ "require": "./dist/auth/index.js"
18
+ },
19
+ "./queue": {
20
+ "types": "./dist/queue/index.d.ts",
21
+ "import": "./dist/queue/index.mjs",
22
+ "require": "./dist/queue/index.js"
23
+ },
24
+ "./notifications": {
25
+ "types": "./dist/notifications/index.d.ts",
26
+ "import": "./dist/notifications/index.mjs",
27
+ "require": "./dist/notifications/index.js"
28
+ },
29
+ "./logger": {
30
+ "types": "./dist/logger/index.d.ts",
31
+ "import": "./dist/logger/index.mjs",
32
+ "require": "./dist/logger/index.js"
33
+ },
34
+ "./rate-limit": {
35
+ "types": "./dist/rate-limit/index.d.ts",
36
+ "import": "./dist/rate-limit/index.mjs",
37
+ "require": "./dist/rate-limit/index.js"
38
+ },
39
+ "./config": {
40
+ "types": "./dist/config/index.d.ts",
41
+ "import": "./dist/config/index.mjs",
42
+ "require": "./dist/config/index.js"
43
+ },
44
+ "./response": {
45
+ "types": "./dist/response/index.d.ts",
46
+ "import": "./dist/response/index.mjs",
47
+ "require": "./dist/response/index.js"
48
+ }
49
+ },
50
+ "scripts": {
51
+ "build": "tsup && node copy-dts.js",
52
+ "dev": "tsup --watch",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "lint": "eslint src --ext .ts",
56
+ "typecheck": "tsc --noEmit",
57
+ "prepublishOnly": "npm run build"
58
+ },
59
+ "keywords": [
60
+ "saas",
61
+ "backend",
62
+ "express",
63
+ "fastify",
64
+ "auth",
65
+ "jwt",
66
+ "redis",
67
+ "queue",
68
+ "notifications",
69
+ "rate-limit"
70
+ ],
71
+ "author": "",
72
+ "license": "MIT",
73
+ "dependencies": {
74
+ "bcryptjs": "^2.4.3",
75
+ "bullmq": "^5.1.0",
76
+ "express": "^4.18.2",
77
+ "fastify": "^4.25.0",
78
+ "ioredis": "^5.3.2",
79
+ "jsonwebtoken": "^9.0.2",
80
+ "nodemailer": "^6.9.8",
81
+ "pino": "^8.17.2",
82
+ "pino-pretty": "^10.3.1",
83
+ "twilio": "^4.20.1",
84
+ "zod": "^3.22.4"
85
+ },
86
+ "devDependencies": {
87
+ "@types/bcryptjs": "^2.4.6",
88
+ "@types/express": "^4.17.21",
89
+ "@types/jsonwebtoken": "^9.0.5",
90
+ "@types/node": "^20.10.6",
91
+ "@types/nodemailer": "^6.4.14",
92
+ "eslint": "^8.56.0",
93
+ "tsup": "^8.0.1",
94
+ "typescript": "^5.3.3",
95
+ "vitest": "^1.1.3"
96
+ },
97
+ "peerDependencies": {
98
+ "express": "^4.0.0",
99
+ "fastify": "^4.0.0"
100
+ },
101
+ "peerDependenciesMeta": {
102
+ "express": {
103
+ "optional": true
104
+ },
105
+ "fastify": {
106
+ "optional": true
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,250 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import { Request, Response, NextFunction, RequestHandler } from 'express';
3
+ import { JWTPayload, LoginCredentials, RegisterData, User, AuthOptions } from './types';
4
+ import { jwtService } from './jwt';
5
+ import { rbacService } from './rbac';
6
+ import { oauthService } from './oauth';
7
+
8
+ export interface AuthUser extends JWTPayload {
9
+ id: string;
10
+ email: string;
11
+ role: string;
12
+ }
13
+
14
+ declare global {
15
+ namespace Express {
16
+ interface Request {
17
+ user?: AuthUser;
18
+ }
19
+ }
20
+ }
21
+
22
+ export interface UserStore {
23
+ findByEmail(email: string): Promise<User | null>;
24
+ findById(id: string): Promise<User | null>;
25
+ create(data: RegisterData): Promise<User>;
26
+ update(id: string, data: Partial<User>): Promise<User>;
27
+ }
28
+
29
+ class InMemoryUserStore implements UserStore {
30
+ private users: Map<string, User> = new Map();
31
+
32
+ async findByEmail(email: string): Promise<User | null> {
33
+ for (const user of this.users.values()) {
34
+ if (user.email === email) return user;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ async findById(id: string): Promise<User | null> {
40
+ return this.users.get(id) || null;
41
+ }
42
+
43
+ async create(data: RegisterData): Promise<User> {
44
+ const id = Math.random().toString(36).substr(2, 9);
45
+ const hashedPassword = await bcrypt.hash(data.password, 10);
46
+ const user: User = {
47
+ id,
48
+ email: data.email,
49
+ password: hashedPassword,
50
+ role: data.role || 'user',
51
+ name: data.name,
52
+ };
53
+ this.users.set(id, user);
54
+ return user;
55
+ }
56
+
57
+ async update(id: string, data: Partial<User>): Promise<User> {
58
+ const user = this.users.get(id);
59
+ if (!user) throw new Error('User not found');
60
+ const updated = { ...user, ...data };
61
+ this.users.set(id, updated);
62
+ return updated;
63
+ }
64
+ }
65
+
66
+ export class AuthService {
67
+ private userStore: UserStore;
68
+ private options: Required<AuthOptions>;
69
+ private initialized: boolean = false;
70
+
71
+ constructor(options: AuthOptions = {}, userStore?: UserStore) {
72
+ this.options = {
73
+ jwtSecret: options.jwtSecret || process.env.JWT_SECRET || 'default-secret-change-in-production',
74
+ jwtExpiresIn: options.jwtExpiresIn || '7d',
75
+ refreshSecret: options.refreshSecret || process.env.JWT_REFRESH_SECRET || 'default-secret-change-in-production',
76
+ refreshExpiresIn: options.refreshExpiresIn || '30d',
77
+ googleClientId: options.googleClientId || process.env.GOOGLE_CLIENT_ID || '',
78
+ googleClientSecret: options.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || '',
79
+ googleRedirectUri: options.googleRedirectUri || process.env.GOOGLE_REDIRECT_URI || '',
80
+ };
81
+
82
+ this.userStore = userStore || new InMemoryUserStore();
83
+ }
84
+
85
+ async initialize(): Promise<void> {
86
+ if (this.options.googleClientId && this.options.googleClientSecret && this.options.googleRedirectUri) {
87
+ oauthService.registerProvider('google', {
88
+ name: 'google',
89
+ clientId: this.options.googleClientId,
90
+ clientSecret: this.options.googleClientSecret,
91
+ redirectUri: this.options.googleRedirectUri,
92
+ authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
93
+ tokenUrl: 'https://oauth2.googleapis.com/token',
94
+ userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
95
+ });
96
+ }
97
+ this.initialized = true;
98
+ }
99
+
100
+ async register(data: RegisterData): Promise<{ user: User; tokens: { accessToken: string; refreshToken: string } }> {
101
+ const existing = await this.userStore.findByEmail(data.email);
102
+ if (existing) {
103
+ throw new Error('User already exists');
104
+ }
105
+
106
+ const user = await this.userStore.create(data);
107
+ const tokens = jwtService.generateTokenPair({
108
+ userId: user.id,
109
+ email: user.email,
110
+ role: user.role,
111
+ });
112
+
113
+ return { user, tokens };
114
+ }
115
+
116
+ async login(credentials: LoginCredentials): Promise<{ user: User; tokens: { accessToken: string; refreshToken: string } }> {
117
+ const user = await this.userStore.findByEmail(credentials.email);
118
+ if (!user || !user.password) {
119
+ throw new Error('Invalid credentials');
120
+ }
121
+
122
+ const isValid = await bcrypt.compare(credentials.password, user.password);
123
+ if (!isValid) {
124
+ throw new Error('Invalid credentials');
125
+ }
126
+
127
+ const tokens = jwtService.generateTokenPair({
128
+ userId: user.id,
129
+ email: user.email,
130
+ role: user.role,
131
+ });
132
+
133
+ return { user, tokens };
134
+ }
135
+
136
+ async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
137
+ return jwtService.refreshTokens(refreshToken);
138
+ }
139
+
140
+ async getGoogleAuthUrl(state?: string): Promise<string> {
141
+ return oauthService.getAuthorizationUrl('google', state);
142
+ }
143
+
144
+ async handleGoogleCallback(code: string): Promise<{ user: User; tokens: { accessToken: string; refreshToken: string } }> {
145
+ const { accessToken } = await oauthService.exchangeCode('google', code);
146
+ const userInfo = await oauthService.getUserInfo('google', accessToken);
147
+
148
+ let user = await this.userStore.findByEmail(userInfo.email);
149
+
150
+ if (!user) {
151
+ user = await this.userStore.create({
152
+ email: userInfo.email,
153
+ password: Math.random().toString(36).substr(2, 20),
154
+ name: userInfo.name,
155
+ role: 'user',
156
+ });
157
+ }
158
+
159
+ const tokens = jwtService.generateTokenPair({
160
+ userId: user.id,
161
+ email: user.email,
162
+ role: user.role,
163
+ });
164
+
165
+ return { user, tokens };
166
+ }
167
+
168
+ getMiddleware(): RequestHandler {
169
+ return (req: Request, res: Response, next: NextFunction) => {
170
+ const authHeader = req.headers.authorization;
171
+
172
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
173
+ return res.status(401).json({ error: 'No token provided' });
174
+ }
175
+
176
+ const token = authHeader.substring(7);
177
+
178
+ try {
179
+ const payload = jwtService.verifyToken(token);
180
+ req.user = {
181
+ ...payload,
182
+ id: payload.userId,
183
+ };
184
+ next();
185
+ } catch (error) {
186
+ return res.status(401).json({ error: 'Invalid token' });
187
+ }
188
+ };
189
+ }
190
+
191
+ requireUser(): RequestHandler {
192
+ return (req: Request, res: Response, next: NextFunction) => {
193
+ if (!req.user) {
194
+ return res.status(401).json({ error: 'Authentication required' });
195
+ }
196
+ next();
197
+ };
198
+ }
199
+
200
+ requireRole(role: string): RequestHandler {
201
+ return (req: Request, res: Response, next: NextFunction) => {
202
+ if (!req.user) {
203
+ return res.status(401).json({ error: 'Authentication required' });
204
+ }
205
+ if (!rbacService.hasRole(req.user.role, role)) {
206
+ return res.status(403).json({ error: 'Insufficient permissions' });
207
+ }
208
+ next();
209
+ };
210
+ }
211
+
212
+ requirePermission(permission: string): RequestHandler {
213
+ return (req: Request, res: Response, next: NextFunction) => {
214
+ if (!req.user) {
215
+ return res.status(401).json({ error: 'Authentication required' });
216
+ }
217
+ if (!rbacService.hasPermission(req.user.role, permission)) {
218
+ return res.status(403).json({ error: 'Insufficient permissions' });
219
+ }
220
+ next();
221
+ };
222
+ }
223
+ }
224
+
225
+ let authServiceInstance: AuthService | null = null;
226
+
227
+ export function createAuth(options?: AuthOptions, userStore?: UserStore): AuthService {
228
+ authServiceInstance = new AuthService(options, userStore);
229
+ return authServiceInstance;
230
+ }
231
+
232
+ export function auth(): AuthService {
233
+ if (!authServiceInstance) {
234
+ authServiceInstance = new AuthService();
235
+ }
236
+ return authServiceInstance;
237
+ }
238
+
239
+ export const Auth = {
240
+ initialize: (options?: AuthOptions) => {
241
+ const service = createAuth(options);
242
+ return {
243
+ service,
244
+ getMiddleware: () => service.getMiddleware(),
245
+ requireUser: () => service.requireUser(),
246
+ requireRole: (role: string) => service.requireRole(role),
247
+ requirePermission: (permission: string) => service.requirePermission(permission),
248
+ };
249
+ },
250
+ };
@@ -0,0 +1,65 @@
1
+ import { FastifyInstance, FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
2
+ import { AuthService } from './express';
3
+ import { JWTPayload } from './types';
4
+
5
+ declare module 'fastify' {
6
+ interface FastifyRequest {
7
+ user?: JWTPayload & { id: string };
8
+ }
9
+ }
10
+
11
+ export function registerAuthPlugin(fastify: FastifyInstance, options: { authService: AuthService }, done: HookHandlerDoneFunction) {
12
+ const { authService } = options;
13
+
14
+ fastify.decorate('authenticate', async (request: FastifyRequest, reply: FastifyReply) => {
15
+ const authHeader = request.headers.authorization;
16
+
17
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
18
+ return reply.status(401).send({ error: 'No token provided' });
19
+ }
20
+
21
+ const token = authHeader.substring(7);
22
+
23
+ try {
24
+ const payload = authService['jwtService'].verifyToken(token);
25
+ request.user = {
26
+ ...payload,
27
+ id: payload.userId,
28
+ };
29
+ } catch (error) {
30
+ return reply.status(401).send({ error: 'Invalid token' });
31
+ }
32
+ });
33
+
34
+ fastify.decorate('requireUser', async (request: FastifyRequest, reply: FastifyReply) => {
35
+ if (!request.user) {
36
+ return reply.status(401).send({ error: 'Authentication required' });
37
+ }
38
+ });
39
+
40
+ fastify.decorate('requireRole', (role: string) => {
41
+ return async (request: FastifyRequest, reply: FastifyReply) => {
42
+ if (!request.user) {
43
+ return reply.status(401).send({ error: 'Authentication required' });
44
+ }
45
+ const { rbacService } = await import('./rbac');
46
+ if (!rbacService.hasRole(request.user.role, role)) {
47
+ return reply.status(403).send({ error: 'Insufficient permissions' });
48
+ }
49
+ };
50
+ });
51
+
52
+ fastify.decorate('requirePermission', (permission: string) => {
53
+ return async (request: FastifyRequest, reply: FastifyReply) => {
54
+ if (!request.user) {
55
+ return reply.status(401).send({ error: 'Authentication required' });
56
+ }
57
+ const { rbacService } = await import('./rbac');
58
+ if (!rbacService.hasPermission(request.user.role, permission)) {
59
+ return reply.status(403).send({ error: 'Insufficient permissions' });
60
+ }
61
+ };
62
+ });
63
+
64
+ done();
65
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './jwt';
3
+ export * from './rbac';
4
+ export * from './oauth';
5
+ export * from './express';
6
+ export * from './fastify';