@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.
- package/.dockerignore +71 -0
- package/.env.example +70 -0
- package/.gitignore +11 -0
- package/LICENSE +15 -0
- package/README.md +231 -0
- package/bin/cli.js +145 -0
- package/config/app.json +166 -0
- package/config/rate-limit.json +12 -0
- package/dist/main.js +26 -0
- package/dockerfile +57 -0
- package/locales/ar_SA.json +179 -0
- package/locales/de_DE.json +179 -0
- package/locales/en_US.json +180 -0
- package/locales/es_ES.json +179 -0
- package/locales/fr_FR.json +179 -0
- package/locales/it_IT.json +179 -0
- package/locales/ja_JP.json +179 -0
- package/locales/pt_BR.json +179 -0
- package/locales/ru_RU.json +179 -0
- package/locales/tr_TR.json +179 -0
- package/main.ts +25 -0
- package/package.json +126 -0
- package/prisma/schema.prisma +9 -0
- package/prisma.config.ts +12 -0
- package/public/static/favicon.ico +0 -0
- package/public/static/image/logo.png +0 -0
- package/routes/api/v1/index.ts +113 -0
- package/src/app.ts +197 -0
- package/src/auth/Auth.ts +446 -0
- package/src/auth/stores/ISessionStore.ts +19 -0
- package/src/auth/stores/MemoryStore.ts +70 -0
- package/src/auth/stores/RedisStore.ts +92 -0
- package/src/auth/stores/index.ts +149 -0
- package/src/config/acl.ts +9 -0
- package/src/config/rls.ts +38 -0
- package/src/core/dmmf.ts +226 -0
- package/src/core/env.ts +183 -0
- package/src/core/errors.ts +87 -0
- package/src/core/i18n.ts +144 -0
- package/src/core/middleware.ts +123 -0
- package/src/core/prisma.ts +236 -0
- package/src/index.ts +112 -0
- package/src/middleware/model.ts +61 -0
- package/src/orm/Model.ts +881 -0
- package/src/orm/QueryBuilder.ts +2078 -0
- package/src/plugins/auth.ts +162 -0
- package/src/plugins/language.ts +79 -0
- package/src/plugins/rateLimit.ts +210 -0
- package/src/plugins/response.ts +80 -0
- package/src/plugins/rls.ts +51 -0
- package/src/plugins/security.ts +23 -0
- package/src/plugins/upload.ts +299 -0
- package/src/types.ts +308 -0
- package/src/utils/ApiClient.ts +526 -0
- package/src/utils/Mailer.ts +348 -0
- package/src/utils/index.ts +25 -0
- package/templates/email/example.ejs +17 -0
- package/templates/layouts/email.ejs +35 -0
- 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;
|