@rapidd/core 2.1.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 (59) hide show
  1. package/.dockerignore +71 -0
  2. package/.env.example +70 -0
  3. package/.gitignore +11 -0
  4. package/LICENSE +15 -0
  5. package/README.md +231 -0
  6. package/bin/cli.js +145 -0
  7. package/config/app.json +166 -0
  8. package/config/rate-limit.json +12 -0
  9. package/dist/main.js +26 -0
  10. package/dockerfile +57 -0
  11. package/locales/ar_SA.json +179 -0
  12. package/locales/de_DE.json +179 -0
  13. package/locales/en_US.json +180 -0
  14. package/locales/es_ES.json +179 -0
  15. package/locales/fr_FR.json +179 -0
  16. package/locales/it_IT.json +179 -0
  17. package/locales/ja_JP.json +179 -0
  18. package/locales/pt_BR.json +179 -0
  19. package/locales/ru_RU.json +179 -0
  20. package/locales/tr_TR.json +179 -0
  21. package/main.ts +25 -0
  22. package/package.json +126 -0
  23. package/prisma/schema.prisma +9 -0
  24. package/prisma.config.ts +12 -0
  25. package/public/static/favicon.ico +0 -0
  26. package/public/static/image/logo.png +0 -0
  27. package/routes/api/v1/index.ts +113 -0
  28. package/src/app.ts +197 -0
  29. package/src/auth/Auth.ts +446 -0
  30. package/src/auth/stores/ISessionStore.ts +19 -0
  31. package/src/auth/stores/MemoryStore.ts +70 -0
  32. package/src/auth/stores/RedisStore.ts +92 -0
  33. package/src/auth/stores/index.ts +149 -0
  34. package/src/config/acl.ts +9 -0
  35. package/src/config/rls.ts +38 -0
  36. package/src/core/dmmf.ts +226 -0
  37. package/src/core/env.ts +183 -0
  38. package/src/core/errors.ts +87 -0
  39. package/src/core/i18n.ts +144 -0
  40. package/src/core/middleware.ts +123 -0
  41. package/src/core/prisma.ts +236 -0
  42. package/src/index.ts +112 -0
  43. package/src/middleware/model.ts +61 -0
  44. package/src/orm/Model.ts +881 -0
  45. package/src/orm/QueryBuilder.ts +2078 -0
  46. package/src/plugins/auth.ts +162 -0
  47. package/src/plugins/language.ts +79 -0
  48. package/src/plugins/rateLimit.ts +210 -0
  49. package/src/plugins/response.ts +80 -0
  50. package/src/plugins/rls.ts +51 -0
  51. package/src/plugins/security.ts +23 -0
  52. package/src/plugins/upload.ts +299 -0
  53. package/src/types.ts +308 -0
  54. package/src/utils/ApiClient.ts +526 -0
  55. package/src/utils/Mailer.ts +348 -0
  56. package/src/utils/index.ts +25 -0
  57. package/templates/email/example.ejs +17 -0
  58. package/templates/layouts/email.ejs +35 -0
  59. package/tsconfig.json +33 -0
@@ -0,0 +1,113 @@
1
+ import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
2
+ import { Auth } from '../../../src/auth/Auth';
3
+ import { ErrorResponse } from '../../../src/core/errors';
4
+
5
+ /**
6
+ * Auth routes — /register, /login, /logout, /refresh, /me
7
+ */
8
+ const rootRoutes: FastifyPluginAsync = async (fastify) => {
9
+ const auth = fastify.auth ?? new Auth();
10
+
11
+ /**
12
+ * POST /register
13
+ * Register a new user.
14
+ *
15
+ * Body:
16
+ * {
17
+ * "email": "user@example.com",
18
+ * "password": "securePassword123",
19
+ * ...additionalFields
20
+ * }
21
+ */
22
+ fastify.post('/register', async (request: FastifyRequest, reply: FastifyReply) => {
23
+ try {
24
+ const { email, password, ...userData } = request.body as Record<string, unknown>;
25
+
26
+ if (!email || !password) {
27
+ return reply.sendError(400, 'email_and_password_required');
28
+ }
29
+
30
+ const User = auth.getUserModel();
31
+ const passwordField = auth.options.passwordField;
32
+
33
+ // Check if user exists
34
+ const existing = await User.findUnique({
35
+ where: { email: email as string },
36
+ }).catch(() => null);
37
+
38
+ if (existing) {
39
+ return reply.sendError(409, 'email_already_exists');
40
+ }
41
+
42
+ // Create user
43
+ const hashedPassword = await auth.hashPassword(password as string);
44
+ const user = await User.create({
45
+ data: {
46
+ email: email as string,
47
+ [passwordField]: hashedPassword,
48
+ ...userData,
49
+ },
50
+ });
51
+
52
+ return reply.sendResponse(201, 'user_registered', { userId: user.id });
53
+ } catch (error: any) {
54
+ if (error instanceof ErrorResponse) {
55
+ throw error;
56
+ }
57
+ return reply.sendError(500, error.message || 'internal_server_error');
58
+ }
59
+ });
60
+
61
+ /**
62
+ * POST /login
63
+ * Login with user (email/username) and password.
64
+ *
65
+ * Body:
66
+ * {
67
+ * "user": "user@example.com",
68
+ * "password": "securePassword123"
69
+ * }
70
+ */
71
+ fastify.post('/login', async (request: FastifyRequest, reply: FastifyReply) => {
72
+ const result = await auth.login(request.body as { user: string; password: string });
73
+ return reply.send(result);
74
+ });
75
+
76
+ /**
77
+ * POST /logout
78
+ * Logout user and delete session.
79
+ */
80
+ fastify.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => {
81
+ const result = await auth.logout(request.headers.authorization);
82
+ return reply.send(result);
83
+ });
84
+
85
+ /**
86
+ * POST /refresh
87
+ * Refresh access token using refresh token.
88
+ *
89
+ * Body:
90
+ * {
91
+ * "refreshToken": "..."
92
+ * }
93
+ */
94
+ fastify.post('/refresh', async (request: FastifyRequest, reply: FastifyReply) => {
95
+ const result = await auth.refresh(request.body as { refreshToken: string });
96
+ return reply.send(result);
97
+ });
98
+
99
+ /**
100
+ * GET /me
101
+ * Get current logged-in user.
102
+ * Requires: Authentication
103
+ */
104
+ fastify.get('/me', async (request: FastifyRequest, reply: FastifyReply) => {
105
+ if (!request.user) {
106
+ return reply.sendError(401, 'no_valid_session');
107
+ }
108
+ const result = await auth.me(request.user);
109
+ return reply.send(result);
110
+ });
111
+ };
112
+
113
+ export default rootRoutes;
package/src/app.ts ADDED
@@ -0,0 +1,197 @@
1
+ import 'dotenv/config';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import Fastify, { FastifyInstance } from 'fastify';
5
+ import fastifyCookie from '@fastify/cookie';
6
+ import fastifyCors from '@fastify/cors';
7
+ import fastifyFormbody from '@fastify/formbody';
8
+ import fastifyStatic from '@fastify/static';
9
+
10
+ import { ErrorResponse } from './core/errors';
11
+ import { LanguageDict } from './core/i18n';
12
+ import { disconnectAll } from './core/prisma';
13
+ import { validateEnv } from './core/env';
14
+ import { env } from './utils';
15
+
16
+ // Plugins
17
+ import securityPlugin from './plugins/security';
18
+ import languagePlugin from './plugins/language';
19
+ import responsePlugin from './plugins/response';
20
+ import authPlugin from './plugins/auth';
21
+ import rlsPlugin from './plugins/rls';
22
+
23
+ import type { RapiddOptions } from './types';
24
+
25
+ // ─── Path Setup ─────────────────────────────────────
26
+ // Use process.cwd() as the project root — works from both source (tsx) and compiled (dist/) contexts.
27
+
28
+ const ROOT = process.env.ROOT || process.cwd();
29
+ process.env.ROOT = ROOT;
30
+ process.env.ROUTES_PATH = process.env.ROUTES_PATH || path.join(ROOT, env.isDevelopment() ? 'routes' : 'dist/routes');
31
+ process.env.STRINGS_PATH = process.env.STRINGS_PATH || path.join(ROOT, 'locales');
32
+ process.env.PUBLIC_PATH = process.env.PUBLIC_PATH || path.join(ROOT, 'public');
33
+ process.env.PUBLIC_STATIC = process.env.PUBLIC_STATIC || path.join(process.env.PUBLIC_PATH!, 'static');
34
+
35
+ const NODE_ENV = process.env.NODE_ENV;
36
+
37
+ // ─── Initialize LanguageDict ────────────────────────
38
+
39
+ LanguageDict.initialize(process.env.STRINGS_PATH, 'en_US');
40
+
41
+ // ─── App Factory ────────────────────────────────────
42
+
43
+ export async function buildApp(options: RapiddOptions = {}): Promise<FastifyInstance> {
44
+ // Validate required environment variables
45
+ validateEnv();
46
+
47
+ const app = Fastify({
48
+ logger: NODE_ENV !== 'test',
49
+ trustProxy: process.env.TRUST_PROXY !== undefined
50
+ ? process.env.TRUST_PROXY === 'true'
51
+ : NODE_ENV === 'production',
52
+ routerOptions: {
53
+ caseSensitive: true,
54
+ },
55
+ });
56
+
57
+ // ── Body Parsing ────────────────────────────────
58
+ await app.register(fastifyFormbody);
59
+
60
+ // ── Static Files ────────────────────────────────
61
+ const staticPath = process.env.PUBLIC_STATIC!;
62
+ if (fs.existsSync(staticPath)) {
63
+ await app.register(fastifyStatic, {
64
+ root: staticPath,
65
+ prefix: '/static/',
66
+ });
67
+ }
68
+
69
+ // ── Cookies ─────────────────────────────────────
70
+ await app.register(fastifyCookie, {
71
+ secret: process.env.COOKIE_SECRET,
72
+ parseOptions: {
73
+ path: '/',
74
+ httpOnly: true,
75
+ secure: NODE_ENV === 'production',
76
+ sameSite: 'strict' as const,
77
+ signed: true,
78
+ },
79
+ });
80
+
81
+ // ── CORS ────────────────────────────────────────
82
+ const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').map((e: string) => e.trim());
83
+
84
+ const corsOptions = NODE_ENV === 'production'
85
+ ? {
86
+ origin: (origin: string, cb: (err: Error | null, origin: boolean) => void) => {
87
+ if (!origin) return cb(null, true);
88
+ let originHost: string;
89
+ try {
90
+ originHost = new URL(origin).hostname;
91
+ } catch {
92
+ return cb(new ErrorResponse(403, 'cors_blocked', { origin }), false);
93
+ }
94
+ const allowed = allowedOrigins.some((e: string) => {
95
+ const trimmed = e.replace(/^https?:\/\//, '');
96
+ return originHost === trimmed || originHost.endsWith(`.${trimmed}`);
97
+ });
98
+ if (allowed) return cb(null, true);
99
+ return cb(new ErrorResponse(403, 'cors_blocked', { origin }), false);
100
+ },
101
+ }
102
+ : { origin: '*' as const };
103
+
104
+ await app.register(fastifyCors, corsOptions as any);
105
+
106
+ // ── Security Headers ────────────────────────────
107
+ await app.register(securityPlugin);
108
+
109
+ // ── Language Resolution ──────────────────────────
110
+ await app.register(languagePlugin);
111
+
112
+ // ── API Decorators & Error Handler ───────────────
113
+ await app.register(responsePlugin);
114
+
115
+ // ── Authentication ──────────────────────────────
116
+ await app.register(authPlugin);
117
+
118
+ // ── RLS Context ─────────────────────────────────
119
+ await app.register(rlsPlugin);
120
+
121
+ // ── Rate Limiting (optional) ────────────────────
122
+ if (options.rateLimit !== false && process.env.RATE_LIMIT_ENABLED !== 'false') {
123
+ const rateLimitPlugin = (await import('./plugins/rateLimit')).default;
124
+ await app.register(rateLimitPlugin);
125
+ }
126
+
127
+ // ── Route Loading ───────────────────────────────
128
+ const routesPath = options.routesPath || process.env.ROUTES_PATH!;
129
+ if (fs.existsSync(routesPath)) {
130
+ await loadRoutes(app, routesPath);
131
+ }
132
+
133
+ // ── 404 Handler ─────────────────────────────────
134
+ app.setNotFoundHandler((_request, reply) => {
135
+ reply.code(404).send({ status_code: 404, message: 'Not found' });
136
+ });
137
+
138
+ // ── Graceful Shutdown ───────────────────────────
139
+ app.addHook('onClose', async () => {
140
+ await disconnectAll();
141
+ });
142
+
143
+ return app;
144
+ }
145
+
146
+ // ─── Route Loader ───────────────────────────────────
147
+
148
+ async function loadRoutes(app: FastifyInstance, routePath: string): Promise<void> {
149
+ const basePath = process.env.ROUTES_PATH!;
150
+ const relativePath = '/' + path.relative(basePath, routePath).replace(/\\/g, '/');
151
+
152
+ const entries = fs.readdirSync(routePath, { withFileTypes: true })
153
+ .sort((a, b) =>
154
+ (a.name === 'index.js' || a.name === 'index.ts' ? -2 : a.isDirectory() ? 0 : -1) -
155
+ (b.name === 'index.js' || b.name === 'index.ts' ? -2 : b.isDirectory() ? 0 : -1)
156
+ );
157
+
158
+ for (const entry of entries) {
159
+ if (entry.isDirectory()) {
160
+ await loadRoutes(app, path.join(routePath, entry.name));
161
+ } else {
162
+ const ext = path.extname(entry.name);
163
+ if ((ext === '.js' || ext === '.ts') && entry.name[0] !== '_' && !entry.name.endsWith('.d.ts')) {
164
+ const isRoot = entry.name === 'index.js' || entry.name === 'index.ts';
165
+ const route = isRoot
166
+ ? relativePath
167
+ : `${relativePath.length > 1 ? relativePath : ''}/${path.parse(entry.name).name}`;
168
+
169
+ const modulePath = path.join(routePath, entry.name);
170
+
171
+ try {
172
+ const routeModule = require(modulePath);
173
+ const plugin = routeModule.default || routeModule;
174
+
175
+ if (typeof plugin === 'function') {
176
+ await app.register(plugin, { prefix: route });
177
+ }
178
+ } catch (err) {
179
+ console.error(`Failed to load route ${route}:`, (err as Error).message);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+
187
+ // Handle uncaught errors
188
+ process.on('uncaughtException', (err) => {
189
+ console.error('[Uncaught Exception]', err);
190
+ process.exit(1);
191
+ });
192
+
193
+ process.on('unhandledRejection', (reason, promise) => {
194
+ console.error('[Unhandled Rejection] at:', promise, 'reason:', reason);
195
+ });
196
+
197
+ export default buildApp;