servcraft 0.1.0 → 0.1.1
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/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Service
|
|
3
|
+
* Redis-based session management with optional Prisma persistence
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import type { Redis } from 'ioredis';
|
|
7
|
+
import { getRedis } from '../../database/redis.js';
|
|
8
|
+
import { prisma } from '../../database/prisma.js';
|
|
9
|
+
import { logger } from '../../core/logger.js';
|
|
10
|
+
import type { Session, CreateSessionData, SessionConfig, SessionStats } from './types.js';
|
|
11
|
+
import { SessionRepository } from './session.repository.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
14
|
+
const DEFAULT_PREFIX = 'session:';
|
|
15
|
+
|
|
16
|
+
export class SessionService {
|
|
17
|
+
private redis: Redis;
|
|
18
|
+
private repository: SessionRepository;
|
|
19
|
+
private config: Required<SessionConfig>;
|
|
20
|
+
|
|
21
|
+
constructor(config: SessionConfig = {}) {
|
|
22
|
+
this.redis = getRedis();
|
|
23
|
+
this.repository = new SessionRepository(prisma);
|
|
24
|
+
this.config = {
|
|
25
|
+
ttlMs: config.ttlMs ?? DEFAULT_TTL_MS,
|
|
26
|
+
prefix: config.prefix ?? DEFAULT_PREFIX,
|
|
27
|
+
persistToDb: config.persistToDb ?? false,
|
|
28
|
+
slidingExpiration: config.slidingExpiration ?? true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
logger.info({ config: this.config }, 'Session service initialized');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new session
|
|
36
|
+
*/
|
|
37
|
+
async create(data: CreateSessionData): Promise<Session> {
|
|
38
|
+
const id = randomUUID();
|
|
39
|
+
const now = new Date();
|
|
40
|
+
const ttlMs = data.expiresInMs ?? this.config.ttlMs;
|
|
41
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
42
|
+
|
|
43
|
+
const session: Session = {
|
|
44
|
+
id,
|
|
45
|
+
userId: data.userId,
|
|
46
|
+
userAgent: data.userAgent,
|
|
47
|
+
ipAddress: data.ipAddress,
|
|
48
|
+
data: data.data,
|
|
49
|
+
expiresAt,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
lastAccessedAt: now,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Store in Redis
|
|
55
|
+
const redisKey = this.buildKey(id);
|
|
56
|
+
const ttlSeconds = Math.ceil(ttlMs / 1000);
|
|
57
|
+
await this.redis.setex(redisKey, ttlSeconds, JSON.stringify(session));
|
|
58
|
+
|
|
59
|
+
// Optionally persist to database
|
|
60
|
+
if (this.config.persistToDb) {
|
|
61
|
+
await this.repository.create({
|
|
62
|
+
id,
|
|
63
|
+
userId: data.userId,
|
|
64
|
+
userAgent: data.userAgent,
|
|
65
|
+
ipAddress: data.ipAddress,
|
|
66
|
+
expiresAt,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.debug({ sessionId: id, userId: data.userId }, 'Session created');
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get session by ID
|
|
76
|
+
*/
|
|
77
|
+
async get(sessionId: string): Promise<Session | null> {
|
|
78
|
+
const redisKey = this.buildKey(sessionId);
|
|
79
|
+
const data = await this.redis.get(redisKey);
|
|
80
|
+
|
|
81
|
+
if (!data) {
|
|
82
|
+
// Try database fallback if persistence is enabled
|
|
83
|
+
if (this.config.persistToDb) {
|
|
84
|
+
const dbSession = await this.repository.findById(sessionId);
|
|
85
|
+
if (dbSession) {
|
|
86
|
+
// Restore to Redis
|
|
87
|
+
const ttlMs = dbSession.expiresAt.getTime() - Date.now();
|
|
88
|
+
if (ttlMs > 0) {
|
|
89
|
+
await this.redis.setex(redisKey, Math.ceil(ttlMs / 1000), JSON.stringify(dbSession));
|
|
90
|
+
return dbSession;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const session = JSON.parse(data) as Session;
|
|
98
|
+
|
|
99
|
+
// Check if expired
|
|
100
|
+
if (new Date(session.expiresAt) < new Date()) {
|
|
101
|
+
await this.destroy(sessionId);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sliding expiration: refresh TTL on access
|
|
106
|
+
if (this.config.slidingExpiration) {
|
|
107
|
+
await this.touch(sessionId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate session and return user ID
|
|
115
|
+
*/
|
|
116
|
+
async validate(sessionId: string): Promise<string | null> {
|
|
117
|
+
const session = await this.get(sessionId);
|
|
118
|
+
return session?.userId ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Update session data
|
|
123
|
+
*/
|
|
124
|
+
async update(sessionId: string, data: Partial<Session['data']>): Promise<Session | null> {
|
|
125
|
+
const session = await this.get(sessionId);
|
|
126
|
+
if (!session) return null;
|
|
127
|
+
|
|
128
|
+
session.data = { ...session.data, ...data };
|
|
129
|
+
session.lastAccessedAt = new Date();
|
|
130
|
+
|
|
131
|
+
const redisKey = this.buildKey(sessionId);
|
|
132
|
+
const ttlMs = session.expiresAt.getTime() - Date.now();
|
|
133
|
+
|
|
134
|
+
if (ttlMs > 0) {
|
|
135
|
+
await this.redis.setex(redisKey, Math.ceil(ttlMs / 1000), JSON.stringify(session));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return session;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Touch session (refresh TTL)
|
|
143
|
+
*/
|
|
144
|
+
async touch(sessionId: string): Promise<boolean> {
|
|
145
|
+
const session = await this.getWithoutTouch(sessionId);
|
|
146
|
+
if (!session) return false;
|
|
147
|
+
|
|
148
|
+
const now = new Date();
|
|
149
|
+
session.lastAccessedAt = now;
|
|
150
|
+
session.expiresAt = new Date(now.getTime() + this.config.ttlMs);
|
|
151
|
+
|
|
152
|
+
const redisKey = this.buildKey(sessionId);
|
|
153
|
+
const ttlSeconds = Math.ceil(this.config.ttlMs / 1000);
|
|
154
|
+
await this.redis.setex(redisKey, ttlSeconds, JSON.stringify(session));
|
|
155
|
+
|
|
156
|
+
// Update database if persistence is enabled
|
|
157
|
+
if (this.config.persistToDb) {
|
|
158
|
+
await this.repository.updateExpiration(sessionId, session.expiresAt);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Destroy session
|
|
166
|
+
*/
|
|
167
|
+
async destroy(sessionId: string): Promise<boolean> {
|
|
168
|
+
const redisKey = this.buildKey(sessionId);
|
|
169
|
+
const deleted = await this.redis.del(redisKey);
|
|
170
|
+
|
|
171
|
+
// Delete from database if persistence is enabled
|
|
172
|
+
if (this.config.persistToDb) {
|
|
173
|
+
await this.repository.delete(sessionId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (deleted > 0) {
|
|
177
|
+
logger.debug({ sessionId }, 'Session destroyed');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return deleted > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Destroy all sessions for a user
|
|
185
|
+
*/
|
|
186
|
+
async destroyUserSessions(userId: string): Promise<number> {
|
|
187
|
+
// Get all sessions for user from database
|
|
188
|
+
const sessions = await this.repository.findByUserId(userId);
|
|
189
|
+
let count = 0;
|
|
190
|
+
|
|
191
|
+
// Delete from Redis
|
|
192
|
+
for (const session of sessions) {
|
|
193
|
+
const redisKey = this.buildKey(session.id);
|
|
194
|
+
const deleted = await this.redis.del(redisKey);
|
|
195
|
+
if (deleted > 0) count++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Delete from database
|
|
199
|
+
if (this.config.persistToDb) {
|
|
200
|
+
await this.repository.deleteByUserId(userId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Also scan Redis for any sessions not in database
|
|
204
|
+
const pattern = `${this.config.prefix}*`;
|
|
205
|
+
const keys = await this.redis.keys(pattern);
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
const data = await this.redis.get(key);
|
|
208
|
+
if (data) {
|
|
209
|
+
const session = JSON.parse(data) as Session;
|
|
210
|
+
if (session.userId === userId) {
|
|
211
|
+
await this.redis.del(key);
|
|
212
|
+
count++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
logger.info({ userId, count }, 'User sessions destroyed');
|
|
218
|
+
return count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get all sessions for a user
|
|
223
|
+
*/
|
|
224
|
+
async getUserSessions(userId: string): Promise<Session[]> {
|
|
225
|
+
const sessions: Session[] = [];
|
|
226
|
+
|
|
227
|
+
// Scan Redis for user sessions
|
|
228
|
+
const pattern = `${this.config.prefix}*`;
|
|
229
|
+
const keys = await this.redis.keys(pattern);
|
|
230
|
+
|
|
231
|
+
for (const key of keys) {
|
|
232
|
+
const data = await this.redis.get(key);
|
|
233
|
+
if (data) {
|
|
234
|
+
const session = JSON.parse(data) as Session;
|
|
235
|
+
if (session.userId === userId && new Date(session.expiresAt) > new Date()) {
|
|
236
|
+
sessions.push(session);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return sessions.sort(
|
|
242
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get session stats
|
|
248
|
+
*/
|
|
249
|
+
async getStats(): Promise<SessionStats> {
|
|
250
|
+
const pattern = `${this.config.prefix}*`;
|
|
251
|
+
const keys = await this.redis.keys(pattern);
|
|
252
|
+
const userSessions = new Map<string, number>();
|
|
253
|
+
let activeSessions = 0;
|
|
254
|
+
|
|
255
|
+
for (const key of keys) {
|
|
256
|
+
const data = await this.redis.get(key);
|
|
257
|
+
if (data) {
|
|
258
|
+
const session = JSON.parse(data) as Session;
|
|
259
|
+
if (new Date(session.expiresAt) > new Date()) {
|
|
260
|
+
activeSessions++;
|
|
261
|
+
const count = userSessions.get(session.userId) || 0;
|
|
262
|
+
userSessions.set(session.userId, count + 1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { activeSessions, userSessions };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Cleanup expired sessions
|
|
272
|
+
*/
|
|
273
|
+
async cleanup(): Promise<number> {
|
|
274
|
+
let count = 0;
|
|
275
|
+
|
|
276
|
+
// Redis handles TTL automatically, but clean database
|
|
277
|
+
if (this.config.persistToDb) {
|
|
278
|
+
count = await this.repository.deleteExpired();
|
|
279
|
+
if (count > 0) {
|
|
280
|
+
logger.info({ count }, 'Cleaned up expired sessions from database');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return count;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Clear all sessions (for testing)
|
|
289
|
+
*/
|
|
290
|
+
async clear(): Promise<void> {
|
|
291
|
+
const pattern = `${this.config.prefix}*`;
|
|
292
|
+
const keys = await this.redis.keys(pattern);
|
|
293
|
+
if (keys.length > 0) {
|
|
294
|
+
await this.redis.del(...keys);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (this.config.persistToDb) {
|
|
298
|
+
await this.repository.clear();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Build Redis key for session
|
|
304
|
+
*/
|
|
305
|
+
private buildKey(sessionId: string): string {
|
|
306
|
+
return `${this.config.prefix}${sessionId}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get session without triggering sliding expiration
|
|
311
|
+
*/
|
|
312
|
+
private async getWithoutTouch(sessionId: string): Promise<Session | null> {
|
|
313
|
+
const redisKey = this.buildKey(sessionId);
|
|
314
|
+
const data = await this.redis.get(redisKey);
|
|
315
|
+
|
|
316
|
+
if (!data) return null;
|
|
317
|
+
|
|
318
|
+
const session = JSON.parse(data) as Session;
|
|
319
|
+
|
|
320
|
+
if (new Date(session.expiresAt) < new Date()) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return session;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let sessionService: SessionService | null = null;
|
|
329
|
+
|
|
330
|
+
export function getSessionService(): SessionService {
|
|
331
|
+
if (!sessionService) {
|
|
332
|
+
sessionService = new SessionService();
|
|
333
|
+
}
|
|
334
|
+
return sessionService;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function createSessionService(config?: SessionConfig): SessionService {
|
|
338
|
+
sessionService = new SessionService(config);
|
|
339
|
+
return sessionService;
|
|
340
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface Session {
|
|
6
|
+
id: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
userAgent?: string;
|
|
9
|
+
ipAddress?: string;
|
|
10
|
+
data?: Record<string, unknown>;
|
|
11
|
+
expiresAt: Date;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
lastAccessedAt?: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CreateSessionData {
|
|
17
|
+
userId: string;
|
|
18
|
+
userAgent?: string;
|
|
19
|
+
ipAddress?: string;
|
|
20
|
+
data?: Record<string, unknown>;
|
|
21
|
+
expiresInMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SessionConfig {
|
|
25
|
+
/** Session TTL in milliseconds (default: 24 hours) */
|
|
26
|
+
ttlMs?: number;
|
|
27
|
+
/** Redis key prefix (default: 'session:') */
|
|
28
|
+
prefix?: string;
|
|
29
|
+
/** Whether to persist sessions to database (default: false) */
|
|
30
|
+
persistToDb?: boolean;
|
|
31
|
+
/** Sliding expiration - refresh TTL on access (default: true) */
|
|
32
|
+
slidingExpiration?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SessionStats {
|
|
36
|
+
activeSessions: number;
|
|
37
|
+
userSessions: Map<string, number>;
|
|
38
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export { registerSwagger, commonResponses, paginationQuery, idParam } from './swagger.service.js';
|
|
2
2
|
export { buildOpenApiSchema, generateRouteSchema } from './schema-builder.js';
|
|
3
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
SwaggerConfig,
|
|
5
|
+
SwaggerTag,
|
|
6
|
+
SwaggerServer,
|
|
7
|
+
RouteSchema,
|
|
8
|
+
EndpointDoc,
|
|
9
|
+
} from './types.js';
|
|
@@ -24,8 +24,16 @@ export function buildOpenApiSchema(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (includeTimestamps) {
|
|
27
|
-
properties.createdAt = {
|
|
28
|
-
|
|
27
|
+
properties.createdAt = {
|
|
28
|
+
type: 'string',
|
|
29
|
+
format: 'date-time',
|
|
30
|
+
description: 'Creation timestamp',
|
|
31
|
+
};
|
|
32
|
+
properties.updatedAt = {
|
|
33
|
+
type: 'string',
|
|
34
|
+
format: 'date-time',
|
|
35
|
+
description: 'Last update timestamp',
|
|
36
|
+
};
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
return {
|
|
@@ -106,7 +114,10 @@ export function generateRouteSchema(
|
|
|
106
114
|
properties: {
|
|
107
115
|
data: {
|
|
108
116
|
type: 'array',
|
|
109
|
-
items: buildOpenApiSchema(fields, {
|
|
117
|
+
items: buildOpenApiSchema(fields, {
|
|
118
|
+
includeId: true,
|
|
119
|
+
includeTimestamps: true,
|
|
120
|
+
}),
|
|
110
121
|
},
|
|
111
122
|
meta: {
|
|
112
123
|
type: 'object',
|
|
@@ -190,7 +201,7 @@ export function generateRouteSchema(
|
|
|
190
201
|
},
|
|
191
202
|
};
|
|
192
203
|
|
|
193
|
-
case 'update':
|
|
204
|
+
case 'update': {
|
|
194
205
|
// Make all fields optional for update
|
|
195
206
|
const optionalFields = fields.map((f) => ({ ...f, isOptional: true }));
|
|
196
207
|
return {
|
|
@@ -226,6 +237,7 @@ export function generateRouteSchema(
|
|
|
226
237
|
},
|
|
227
238
|
},
|
|
228
239
|
};
|
|
240
|
+
}
|
|
229
241
|
|
|
230
242
|
case 'delete':
|
|
231
243
|
return {
|
|
@@ -6,8 +6,6 @@ import { logger } from '../../core/logger.js';
|
|
|
6
6
|
import type { SwaggerConfig } from './types.js';
|
|
7
7
|
|
|
8
8
|
const defaultConfig: SwaggerConfig = {
|
|
9
|
-
enabled: true,
|
|
10
|
-
route: '/docs',
|
|
11
9
|
title: 'Servcraft API',
|
|
12
10
|
description: 'API documentation generated by Servcraft',
|
|
13
11
|
version: '1.0.0',
|
|
@@ -24,11 +22,6 @@ export async function registerSwagger(
|
|
|
24
22
|
): Promise<void> {
|
|
25
23
|
const swaggerConfig = { ...defaultConfig, ...customConfig };
|
|
26
24
|
|
|
27
|
-
if (swaggerConfig.enabled === false) {
|
|
28
|
-
logger.info('Swagger documentation disabled');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
await app.register(swagger, {
|
|
33
26
|
openapi: {
|
|
34
27
|
openapi: '3.0.3',
|
|
@@ -60,7 +53,7 @@ export async function registerSwagger(
|
|
|
60
53
|
});
|
|
61
54
|
|
|
62
55
|
await app.register(swaggerUi, {
|
|
63
|
-
routePrefix:
|
|
56
|
+
routePrefix: '/docs',
|
|
64
57
|
uiConfig: {
|
|
65
58
|
docExpansion: 'list',
|
|
66
59
|
deepLinking: true,
|
|
@@ -77,7 +70,7 @@ export async function registerSwagger(
|
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
// Helper to generate schema from Zod
|
|
80
|
-
export function zodToJsonSchema(
|
|
73
|
+
export function zodToJsonSchema(_zodSchema: unknown): Record<string, unknown> {
|
|
81
74
|
// This is a simplified version - for full support use zod-to-json-schema package
|
|
82
75
|
return {
|
|
83
76
|
type: 'object',
|
|
@@ -152,7 +145,13 @@ export const paginationQuery = {
|
|
|
152
145
|
type: 'object',
|
|
153
146
|
properties: {
|
|
154
147
|
page: { type: 'integer', minimum: 1, default: 1, description: 'Page number' },
|
|
155
|
-
limit: {
|
|
148
|
+
limit: {
|
|
149
|
+
type: 'integer',
|
|
150
|
+
minimum: 1,
|
|
151
|
+
maximum: 100,
|
|
152
|
+
default: 20,
|
|
153
|
+
description: 'Items per page',
|
|
154
|
+
},
|
|
156
155
|
sortBy: { type: 'string', description: 'Field to sort by' },
|
|
157
156
|
sortOrder: { type: 'string', enum: ['asc', 'desc'], default: 'asc', description: 'Sort order' },
|
|
158
157
|
search: { type: 'string', description: 'Search query' },
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { UploadService, getUploadService, createUploadService } from './upload.service.js';
|
|
2
|
+
export { registerUploadRoutes } from './upload.routes.js';
|
|
3
|
+
export type {
|
|
4
|
+
UploadedFile,
|
|
5
|
+
UploadConfig,
|
|
6
|
+
UploadOptions,
|
|
7
|
+
MultipartFile,
|
|
8
|
+
StorageProvider,
|
|
9
|
+
ImageTransformOptions,
|
|
10
|
+
LocalStorageConfig,
|
|
11
|
+
S3Config,
|
|
12
|
+
CloudinaryConfig,
|
|
13
|
+
GCSConfig,
|
|
14
|
+
} from './types.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type StorageProvider = 'local' | 's3' | 'cloudinary' | 'gcs';
|
|
2
|
+
|
|
3
|
+
export interface UploadedFile {
|
|
4
|
+
id: string;
|
|
5
|
+
originalName: string;
|
|
6
|
+
filename: string;
|
|
7
|
+
mimetype: string;
|
|
8
|
+
size: number;
|
|
9
|
+
path: string;
|
|
10
|
+
url: string;
|
|
11
|
+
provider: StorageProvider;
|
|
12
|
+
bucket?: string;
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
userId?: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UploadConfig {
|
|
19
|
+
provider: StorageProvider;
|
|
20
|
+
maxFileSize: number; // in bytes
|
|
21
|
+
allowedMimeTypes: string[];
|
|
22
|
+
local?: LocalStorageConfig;
|
|
23
|
+
s3?: S3Config;
|
|
24
|
+
cloudinary?: CloudinaryConfig;
|
|
25
|
+
gcs?: GCSConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LocalStorageConfig {
|
|
29
|
+
uploadDir: string;
|
|
30
|
+
publicPath: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface S3Config {
|
|
34
|
+
accessKeyId: string;
|
|
35
|
+
secretAccessKey: string;
|
|
36
|
+
region: string;
|
|
37
|
+
bucket: string;
|
|
38
|
+
endpoint?: string; // For S3-compatible services
|
|
39
|
+
acl?: 'private' | 'public-read' | 'public-read-write';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CloudinaryConfig {
|
|
43
|
+
cloudName: string;
|
|
44
|
+
apiKey: string;
|
|
45
|
+
apiSecret: string;
|
|
46
|
+
folder?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GCSConfig {
|
|
50
|
+
projectId: string;
|
|
51
|
+
keyFilename?: string;
|
|
52
|
+
credentials?: {
|
|
53
|
+
client_email: string;
|
|
54
|
+
private_key: string;
|
|
55
|
+
};
|
|
56
|
+
bucket: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ImageTransformOptions {
|
|
60
|
+
width?: number;
|
|
61
|
+
height?: number;
|
|
62
|
+
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
|
|
63
|
+
format?: 'jpeg' | 'png' | 'webp' | 'avif';
|
|
64
|
+
quality?: number;
|
|
65
|
+
blur?: number;
|
|
66
|
+
grayscale?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface UploadOptions {
|
|
70
|
+
filename?: string;
|
|
71
|
+
folder?: string;
|
|
72
|
+
isPublic?: boolean;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
transform?: ImageTransformOptions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MultipartFile {
|
|
78
|
+
filename: string;
|
|
79
|
+
mimetype: string;
|
|
80
|
+
encoding: string;
|
|
81
|
+
file: NodeJS.ReadableStream;
|
|
82
|
+
toBuffer(): Promise<Buffer>;
|
|
83
|
+
}
|