lapeeh 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 (76) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/bin/index.js +934 -0
  4. package/doc/en/ARCHITECTURE_GUIDE.md +79 -0
  5. package/doc/en/CHANGELOG.md +203 -0
  6. package/doc/en/CHEATSHEET.md +90 -0
  7. package/doc/en/CLI.md +111 -0
  8. package/doc/en/CONTRIBUTING.md +119 -0
  9. package/doc/en/DEPLOYMENT.md +171 -0
  10. package/doc/en/FAQ.md +69 -0
  11. package/doc/en/FEATURES.md +99 -0
  12. package/doc/en/GETTING_STARTED.md +84 -0
  13. package/doc/en/INTRODUCTION.md +62 -0
  14. package/doc/en/PACKAGES.md +63 -0
  15. package/doc/en/PERFORMANCE.md +98 -0
  16. package/doc/en/ROADMAP.md +104 -0
  17. package/doc/en/SECURITY.md +95 -0
  18. package/doc/en/STRUCTURE.md +79 -0
  19. package/doc/en/TUTORIAL.md +145 -0
  20. package/doc/id/ARCHITECTURE_GUIDE.md +76 -0
  21. package/doc/id/CHANGELOG.md +203 -0
  22. package/doc/id/CHEATSHEET.md +90 -0
  23. package/doc/id/CLI.md +139 -0
  24. package/doc/id/CONTRIBUTING.md +119 -0
  25. package/doc/id/DEPLOYMENT.md +171 -0
  26. package/doc/id/FAQ.md +69 -0
  27. package/doc/id/FEATURES.md +169 -0
  28. package/doc/id/GETTING_STARTED.md +91 -0
  29. package/doc/id/INTRODUCTION.md +62 -0
  30. package/doc/id/PACKAGES.md +63 -0
  31. package/doc/id/PERFORMANCE.md +100 -0
  32. package/doc/id/ROADMAP.md +107 -0
  33. package/doc/id/SECURITY.md +94 -0
  34. package/doc/id/STRUCTURE.md +79 -0
  35. package/doc/id/TUTORIAL.md +145 -0
  36. package/docker-compose.yml +24 -0
  37. package/ecosystem.config.js +17 -0
  38. package/eslint.config.mjs +26 -0
  39. package/gitignore.template +30 -0
  40. package/lib/bootstrap.ts +210 -0
  41. package/lib/core/realtime.ts +34 -0
  42. package/lib/core/redis.ts +139 -0
  43. package/lib/core/serializer.ts +63 -0
  44. package/lib/core/server.ts +70 -0
  45. package/lib/core/store.ts +116 -0
  46. package/lib/middleware/auth.ts +63 -0
  47. package/lib/middleware/error.ts +50 -0
  48. package/lib/middleware/multipart.ts +13 -0
  49. package/lib/middleware/rateLimit.ts +14 -0
  50. package/lib/middleware/requestLogger.ts +27 -0
  51. package/lib/middleware/visitor.ts +178 -0
  52. package/lib/utils/logger.ts +100 -0
  53. package/lib/utils/pagination.ts +56 -0
  54. package/lib/utils/response.ts +88 -0
  55. package/lib/utils/validator.ts +394 -0
  56. package/nodemon.json +6 -0
  57. package/package.json +126 -0
  58. package/readme.md +357 -0
  59. package/scripts/check-update.js +92 -0
  60. package/scripts/config-clear.js +45 -0
  61. package/scripts/generate-jwt-secret.js +38 -0
  62. package/scripts/init-project.js +84 -0
  63. package/scripts/make-module.js +89 -0
  64. package/scripts/release.js +494 -0
  65. package/scripts/seed-json.js +158 -0
  66. package/scripts/verify-rbac-functional.js +187 -0
  67. package/src/config/app.ts +9 -0
  68. package/src/config/cors.ts +5 -0
  69. package/src/modules/Auth/auth.controller.ts +519 -0
  70. package/src/modules/Rbac/rbac.controller.ts +533 -0
  71. package/src/routes/auth.ts +74 -0
  72. package/src/routes/index.ts +7 -0
  73. package/src/routes/rbac.ts +42 -0
  74. package/storage/logs/.gitkeep +0 -0
  75. package/tsconfig.build.json +12 -0
  76. package/tsconfig.json +30 -0
@@ -0,0 +1,210 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+
4
+ import moduleAlias from "module-alias";
5
+ import express, { Request, Response, NextFunction } from "express";
6
+ import cors from "cors";
7
+ import helmet from "helmet";
8
+ import compression from "compression";
9
+ import http from "http";
10
+ import path from "path";
11
+ import { initRealtime } from "./core/realtime";
12
+ import { initRedis, redis } from "./core/redis";
13
+ import { visitorCounter } from "./middleware/visitor";
14
+ import { errorHandler } from "./middleware/error";
15
+ import { apiLimiter } from "./middleware/rateLimit";
16
+ import { requestLogger } from "./middleware/requestLogger";
17
+ import { sendSuccess } from "./utils/response";
18
+
19
+ export async function createApp() {
20
+ // Register aliases for production runtime
21
+ // Since user code (compiled JS) uses require('@lapeeh/...')
22
+ // We map '@lapeeh' to the directory containing this file (lib/ or dist/lib/)
23
+ moduleAlias.addAlias("@lapeeh", __dirname);
24
+
25
+ // Register alias for src directory (@/) to support imports in controllers/routes
26
+ const isProduction = process.env.NODE_ENV === "production";
27
+ moduleAlias.addAlias(
28
+ "@",
29
+ isProduction
30
+ ? path.join(process.cwd(), "dist", "src")
31
+ : path.join(process.cwd(), "src")
32
+ );
33
+
34
+ // LOAD USER CONFIG
35
+ const configPath = isProduction
36
+ ? path.join(process.cwd(), "dist", "src", "config")
37
+ : path.join(process.cwd(), "src", "config");
38
+
39
+ let appConfig: any = { timeout: 30000, jsonLimit: "10mb" };
40
+ let corsConfig: any = {
41
+ origin: process.env.CORS_ORIGIN || "*",
42
+ credentials: true,
43
+ exposedHeaders: ["x-access-token", "x-access-expires-at"],
44
+ };
45
+
46
+ try {
47
+ const appConfModule = require(path.join(configPath, "app"));
48
+ if (appConfModule.appConfig)
49
+ appConfig = { ...appConfig, ...appConfModule.appConfig };
50
+ } catch (e) {
51
+ // ignore
52
+ }
53
+
54
+ try {
55
+ const corsConfModule = require(path.join(configPath, "cors"));
56
+ if (corsConfModule.corsConfig)
57
+ corsConfig = { ...corsConfig, ...corsConfModule.corsConfig };
58
+ } catch (e) {
59
+ // ignore
60
+ }
61
+
62
+ const app = express();
63
+
64
+ app.disable("x-powered-by");
65
+ app.use(compression());
66
+
67
+ // Request Timeout Middleware
68
+ app.use((_req: Request, res: Response, next: NextFunction) => {
69
+ const timeout = appConfig.timeout || 30000;
70
+ res.setTimeout(timeout, () => {
71
+ res.status(408).send({
72
+ status: "error",
73
+ message: `Request Timeout (${timeout / 1000}s limit)`,
74
+ });
75
+ });
76
+ next();
77
+ });
78
+
79
+ app.use(
80
+ helmet({
81
+ contentSecurityPolicy: false,
82
+ crossOriginResourcePolicy: { policy: "cross-origin" },
83
+ })
84
+ );
85
+
86
+ app.use(cors(corsConfig));
87
+
88
+ app.use(requestLogger);
89
+ app.use(express.json({ limit: appConfig.jsonLimit || "10mb" }));
90
+ app.use(
91
+ express.urlencoded({ extended: true, limit: appConfig.jsonLimit || "10mb" })
92
+ );
93
+ app.use(apiLimiter);
94
+ app.use(visitorCounter);
95
+
96
+ // Health Check
97
+ app.get("/", (_req: Request, res: Response) => {
98
+ sendSuccess(res, 200, "lapeeh API is running", {
99
+ status: "active",
100
+ timestamp: new Date(),
101
+ version: process.env.npm_package_version || "unknown",
102
+ });
103
+ });
104
+
105
+ // DYNAMIC ROUTE LOADING
106
+ try {
107
+ console.log("BOOTSTRAP: Loading routes. NODE_ENV=", process.env.NODE_ENV);
108
+ const isProduction = process.env.NODE_ENV === "production";
109
+ let userRoutesPath = isProduction
110
+ ? path.join(process.cwd(), "dist", "src", "routes")
111
+ : path.join(process.cwd(), "src", "routes");
112
+
113
+ // In test environment, explicitly point to index to ensure resolution
114
+ if (process.env.NODE_ENV === "test") {
115
+ // In test environment (ts-jest), we need to point to the TS file
116
+ // And we might need to use the full path with extension
117
+ userRoutesPath = path.join(process.cwd(), "src", "routes", "index.ts");
118
+ }
119
+
120
+ // Gunakan require agar sinkron dan mudah dicatch
121
+ // Check if file exists before requiring to avoid crash in tests/clean env
122
+ try {
123
+ const { apiRouter } = require(userRoutesPath);
124
+ app.use("/api", apiRouter);
125
+ } catch (e) {
126
+ // If it's just missing module, maybe we are in test mode or fresh install
127
+ if (process.env.NODE_ENV !== "test") {
128
+ console.warn(
129
+ `⚠️ Could not load user routes from ${userRoutesPath}. (This is expected during initial setup or if src/routes is missing)`
130
+ );
131
+ } else {
132
+ // In test mode, we really want to know if it failed to load
133
+ console.error(
134
+ `Error loading routes in test mode from ${userRoutesPath}:`,
135
+ e
136
+ );
137
+ throw e;
138
+ }
139
+ }
140
+ } catch (error) {
141
+ console.error(error);
142
+ if (process.env.NODE_ENV === "test") throw error;
143
+ }
144
+
145
+ app.use(errorHandler);
146
+
147
+ return app;
148
+ }
149
+
150
+ export async function bootstrap() {
151
+ // Validasi Environment Variables
152
+ const requiredEnvs = ["JWT_SECRET"];
153
+ const missingEnvs = requiredEnvs.filter((key) => !process.env[key]);
154
+ if (missingEnvs.length > 0) {
155
+ console.error(
156
+ `❌ Missing required environment variables: ${missingEnvs.join(", ")}`
157
+ );
158
+ process.exit(1);
159
+ }
160
+
161
+ const app = await createApp();
162
+ const port = process.env.PORT ? Number(process.env.PORT) : 8000;
163
+ const server = http.createServer(app);
164
+
165
+ initRealtime(server);
166
+
167
+ try {
168
+ await initRedis();
169
+
170
+ server.on("error", (e: any) => {
171
+ if (e.code === "EADDRINUSE") {
172
+ console.log(`\n❌ Error: Port ${port} is already in use.`);
173
+ process.exit(1);
174
+ }
175
+ });
176
+
177
+ server.listen(port, () => {
178
+ console.log(`✅ API running at http://localhost:${port}`);
179
+ console.log(`🛡️ Environment: ${process.env.NODE_ENV || "development"}`);
180
+ });
181
+ } catch (error) {
182
+ console.error("❌ Failed to start server:", error);
183
+ process.exit(1);
184
+ }
185
+
186
+ // Graceful Shutdown
187
+ const shutdown = async (signal: string) => {
188
+ console.log(`\n🛑 ${signal} received. Closing resources...`);
189
+ server.close(() => console.log("Http server closed."));
190
+ try {
191
+ if (redis && redis.status === "ready") await redis.quit();
192
+ process.exit(0);
193
+ } catch (err) {
194
+ console.error("Error during shutdown:", err);
195
+ process.exit(1);
196
+ }
197
+ };
198
+
199
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
200
+ process.on("SIGINT", () => shutdown("SIGINT"));
201
+ process.on("uncaughtException", (error) => {
202
+ console.error("❌ Uncaught Exception:", error);
203
+ shutdown("uncaughtException");
204
+ });
205
+ }
206
+
207
+ // Self-executing if run directly
208
+ if (require.main === module) {
209
+ bootstrap();
210
+ }
@@ -0,0 +1,34 @@
1
+ import { Server } from "socket.io";
2
+ import jwt from "jsonwebtoken";
3
+
4
+ let io: Server | null = null;
5
+
6
+ export function initRealtime(server: import("http").Server) {
7
+ io = new Server(server, {
8
+ cors: { origin: "*", methods: ["GET", "POST", "PUT", "DELETE"] },
9
+ });
10
+ io.on("connection", (socket) => {
11
+ const token =
12
+ (socket.handshake.query?.token as string) ||
13
+ socket.handshake.headers["authorization"]
14
+ ?.toString()
15
+ ?.replace("Bearer ", "") ||
16
+ "";
17
+ const secret = process.env.JWT_SECRET;
18
+ if (secret && token) {
19
+ try {
20
+ const payload = jwt.verify(token, secret) as {
21
+ userId: string;
22
+ role: string;
23
+ };
24
+ const room = `user:${payload.userId}`;
25
+ socket.join(room);
26
+ } catch {}
27
+ }
28
+ });
29
+ }
30
+
31
+ export function notifyUser(userId: string, event: string, payload: any) {
32
+ if (!io) return;
33
+ io.to(`user:${userId}`).emit(event, payload);
34
+ }
@@ -0,0 +1,139 @@
1
+ import Redis from "ioredis";
2
+ // @ts-ignore
3
+ import RedisMock from "ioredis-mock";
4
+
5
+ const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
6
+
7
+ // Create a wrapper to handle connection attempts
8
+ let redis: Redis;
9
+ let isRedisConnected = false;
10
+
11
+ // If explicitly disabled via env
12
+ if (process.env.NO_REDIS === "true") {
13
+ console.log("Redis disabled via NO_REDIS, using in-memory mock.");
14
+ redis = new RedisMock();
15
+ isRedisConnected = true;
16
+ } else {
17
+ // Try to connect to real Redis
18
+ redis = new Redis(redisUrl, {
19
+ lazyConnect: true,
20
+ maxRetriesPerRequest: 1,
21
+ retryStrategy: (times) => {
22
+ // Retry 3 times then give up
23
+ if (times > 3) return null;
24
+ return 200;
25
+ },
26
+ });
27
+ }
28
+
29
+ redis.on("ready", () => {
30
+ isRedisConnected = true;
31
+ // console.log("Redis connected!");
32
+ });
33
+
34
+ redis.on("error", (_err) => {
35
+ // If connection fails and we haven't switched to mock yet
36
+ if (!isRedisConnected && !(redis instanceof RedisMock)) {
37
+ // console.log("Redis connection failed, switching to in-memory mock...");
38
+ // Replace the global redis instance with mock
39
+ // Note: This is a runtime switch. Existing listeners might be lost if we don't handle carefully.
40
+ // However, for a simple fallback, we can just use the mock for future calls.
41
+ // Better approach: Since we exported 'redis' as a const (reference), we can't reassign it easily
42
+ // if other modules already imported it.
43
+ // BUT, ioredis instance itself is an EventEmitter.
44
+ // Strategy: We keep 'redis' as the main interface.
45
+ // If real redis fails, we just don't set isRedisConnected to true for the *real* one.
46
+ // But wait, the user wants 'bundle redis'.
47
+ // The best way is to detect failure during init and SWAP the implementation.
48
+ }
49
+ isRedisConnected = false;
50
+ });
51
+
52
+ // We need a way to seamlessly switch or just default to Mock if connect fails.
53
+ // Since 'redis' is exported immediately, we can't easily swap the object reference for importers.
54
+ // PROXY APPROACH:
55
+ // We export a Proxy that forwards to real redis OR mock redis.
56
+
57
+ const mockRedis = new RedisMock();
58
+ let activeRedis = redis; // Start with real redis attempt
59
+
60
+ // Custom init function to determine which one to use
61
+ export async function initRedis() {
62
+ if (process.env.NO_REDIS === "true") {
63
+ activeRedis = mockRedis;
64
+ console.log("✅ Redis: Active (Source: Zero-Config Redis [NO_REDIS=true])");
65
+ if (process.env.NODE_ENV === "production") {
66
+ console.warn(
67
+ "⚠️ WARNING: Running in PRODUCTION with in-memory Redis mock. Data will be lost on restart and not shared between instances."
68
+ );
69
+ }
70
+ return;
71
+ }
72
+
73
+ try {
74
+ await redis.connect();
75
+ activeRedis = redis; // Keep using real redis
76
+ isRedisConnected = true;
77
+
78
+ // Determine source label
79
+ const sourceLabel = process.env.REDIS_URL
80
+ ? redisUrl
81
+ : "Zero-Config Redis (Localhost)";
82
+
83
+ console.log(`✅ Redis: Active (Source: ${sourceLabel})`);
84
+ } catch (err) {
85
+ // Connection failed, switch to mock
86
+ console.log(
87
+ `⚠️ Redis: Connection failed to ${redisUrl}, switching to fallback (Source: Zero-Config Redis [Mock])`
88
+ );
89
+ activeRedis = mockRedis;
90
+ isRedisConnected = true; // Mock is always "connected"
91
+ if (process.env.NODE_ENV === "production") {
92
+ console.warn(
93
+ "⚠️ WARNING: Redis connection failed in PRODUCTION. Switched to in-memory mock. Data will be lost on restart."
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ // Proxy handler to forward all calls to activeRedis
100
+ const redisProxy = new Proxy({} as Redis, {
101
+ get: (_target, prop) => {
102
+ // If accessing a property on the proxy, forward it to activeRedis
103
+ const value = (activeRedis as any)[prop];
104
+ return value;
105
+ },
106
+ });
107
+
108
+ export async function getCache(key: string) {
109
+ try {
110
+ const v = await activeRedis.get(key);
111
+ return v ? JSON.parse(v) : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ export async function setCache(key: string, value: any, ttlSeconds = 60) {
118
+ try {
119
+ await activeRedis.set(key, JSON.stringify(value), "EX", ttlSeconds);
120
+ } catch {}
121
+ }
122
+
123
+ export async function delCache(key: string) {
124
+ try {
125
+ await activeRedis.del(key);
126
+ } catch {}
127
+ }
128
+
129
+ export async function delCachePattern(pattern: string) {
130
+ try {
131
+ const keys = await activeRedis.keys(pattern);
132
+ if (keys.length > 0) {
133
+ await activeRedis.del(...keys);
134
+ }
135
+ } catch {}
136
+ }
137
+
138
+ // Export the proxy as 'redis' so consumers use it transparently
139
+ export { redisProxy as redis };
@@ -0,0 +1,63 @@
1
+ import fastJson from "fast-json-stringify";
2
+
3
+ // Cache untuk menyimpan fungsi stringify yang sudah dicompile
4
+ // Key: Nama schema/Identifier, Value: Fungsi stringify
5
+ const serializerCache = new Map<string, (doc: any) => string>();
6
+
7
+ /**
8
+ * Membuat atau mengambil serializer yang sudah dicompile.
9
+ *
10
+ * @param key Identifier unik untuk schema (misal: 'UserResponse', 'ProductList')
11
+ * @param schema JSON Schema definition (Standard JSON Schema)
12
+ * @returns Fungsi yang mengubah object menjadi JSON string dengan sangat cepat
13
+ */
14
+ export function getSerializer(key: string, schema: any) {
15
+ if (serializerCache.has(key)) {
16
+ return serializerCache.get(key)!;
17
+ }
18
+
19
+ const stringify = fastJson(schema);
20
+ serializerCache.set(key, stringify);
21
+ return stringify;
22
+ }
23
+
24
+ /**
25
+ * Helper untuk mendefinisikan schema standar response lapeeh
26
+ * { status: "success", message: string, data: T }
27
+ */
28
+ export function createResponseSchema(dataSchema: any) {
29
+ return {
30
+ title: "StandardResponse",
31
+ type: "object",
32
+ properties: {
33
+ status: { type: "string" },
34
+ message: { type: "string" },
35
+ data: dataSchema,
36
+ },
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Helper khusus untuk response paginasi
42
+ * { status: "success", message: string, data: { data: T[], meta: ... } }
43
+ */
44
+ export function createPaginatedResponseSchema(itemSchema: any) {
45
+ return createResponseSchema({
46
+ type: "object",
47
+ properties: {
48
+ data: {
49
+ type: "array",
50
+ items: itemSchema,
51
+ },
52
+ meta: {
53
+ type: "object",
54
+ properties: {
55
+ page: { type: "integer" },
56
+ perPage: { type: "integer" },
57
+ total: { type: "integer" },
58
+ lastPage: { type: "integer" },
59
+ },
60
+ },
61
+ },
62
+ });
63
+ }
@@ -0,0 +1,70 @@
1
+ import express, { Request, Response, NextFunction } from "express";
2
+ import cors from "cors";
3
+ import helmet from "helmet";
4
+ import compression from "compression";
5
+ // import { apiRouter } from "@/routes"; // Routes are now loaded dynamically in bootstrap.ts
6
+ import { visitorCounter } from "../middleware/visitor";
7
+ // import { errorHandler } from "../middleware/error";
8
+ import { apiLimiter } from "../middleware/rateLimit";
9
+ import { requestLogger } from "../middleware/requestLogger";
10
+ import { sendSuccess } from "../utils/response";
11
+
12
+ export const app = express();
13
+
14
+ app.disable("x-powered-by");
15
+
16
+ // Compression (Gzip)
17
+ app.use(compression());
18
+
19
+ // Request Timeout Middleware (30s)
20
+ app.use((_req: Request, res: Response, next: NextFunction) => {
21
+ res.setTimeout(30000, () => {
22
+ res.status(408).send({
23
+ status: "error",
24
+ message: "Request Timeout (30s limit)",
25
+ });
26
+ });
27
+ next();
28
+ });
29
+
30
+ // Security Headers
31
+ app.use(
32
+ helmet({
33
+ contentSecurityPolicy: false, // Disarankan true jika menggunakan frontend di domain yang sama
34
+ crossOriginResourcePolicy: { policy: "cross-origin" },
35
+ })
36
+ );
37
+
38
+ const corsOrigin = process.env.CORS_ORIGIN || "*";
39
+ app.use(
40
+ cors({
41
+ origin: corsOrigin,
42
+ credentials: true,
43
+ exposedHeaders: ["x-access-token", "x-access-expires-at"],
44
+ })
45
+ );
46
+
47
+ // Logging & Parsing
48
+ app.use(requestLogger);
49
+ app.use(express.json({ limit: "10mb" })); // Limit dinaikkan untuk upload file base64/besar
50
+ app.use(express.urlencoded({ extended: true, limit: "10mb" }));
51
+
52
+ // Rate Limiting (Global)
53
+ app.use(apiLimiter);
54
+
55
+ app.use(visitorCounter);
56
+
57
+ // Health Check Endpoint
58
+ app.get("/", (_req: Request, res: Response) => {
59
+ sendSuccess(res, 200, "lapeeh API is running", {
60
+ status: "active",
61
+ timestamp: new Date(),
62
+ version: process.env.npm_package_version || "2.1.6",
63
+ });
64
+ });
65
+
66
+ // Routes are loaded in bootstrap.ts via app.use('/api', userApiRouter)
67
+
68
+ // Global Error Handler
69
+ // Note: We don't attach error handler here because we want to attach it AFTER routes are loaded in bootstrap
70
+ // app.use(errorHandler);
@@ -0,0 +1,116 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export interface User {
5
+ id: string;
6
+ email: string;
7
+ name: string;
8
+ password?: string;
9
+ uuid: string;
10
+ avatar?: string | null;
11
+ avatar_url?: string | null;
12
+ email_verified_at?: string | Date | null;
13
+ created_at: string | Date;
14
+ updated_at: string | Date;
15
+ }
16
+
17
+ export interface Role {
18
+ id: string;
19
+ name: string;
20
+ slug: string;
21
+ description?: string | null;
22
+ created_at: string | Date;
23
+ updated_at: string | Date;
24
+ }
25
+
26
+ export interface Permission {
27
+ id: string;
28
+ name: string;
29
+ slug: string;
30
+ description?: string | null;
31
+ created_at: string | Date;
32
+ updated_at: string | Date;
33
+ }
34
+
35
+ export interface UserRole {
36
+ id: string;
37
+ user_id: string;
38
+ role_id: string;
39
+ created_at: string | Date;
40
+ }
41
+
42
+ export interface RolePermission {
43
+ id: string;
44
+ role_id: string;
45
+ permission_id: string;
46
+ created_at: string | Date;
47
+ }
48
+
49
+ export interface UserPermission {
50
+ id: string;
51
+ user_id: string;
52
+ permission_id: string;
53
+ created_at: string | Date;
54
+ }
55
+
56
+ // Database file path
57
+ const dbPath = path.resolve(process.cwd(), "database.json");
58
+
59
+ // Load data function
60
+ function loadData() {
61
+ if (fs.existsSync(dbPath)) {
62
+ const raw = fs.readFileSync(dbPath, "utf-8");
63
+ return JSON.parse(raw);
64
+ }
65
+ return {
66
+ users: [],
67
+ roles: [
68
+ {
69
+ id: "1",
70
+ name: "Admin",
71
+ slug: "admin",
72
+ description: "Administrator",
73
+ created_at: new Date(),
74
+ updated_at: new Date(),
75
+ },
76
+ {
77
+ id: "2",
78
+ name: "User",
79
+ slug: "user",
80
+ description: "Standard User",
81
+ created_at: new Date(),
82
+ updated_at: new Date(),
83
+ },
84
+ ],
85
+ permissions: [],
86
+ user_roles: [],
87
+ role_permissions: [],
88
+ user_permissions: [],
89
+ };
90
+ }
91
+
92
+ const data = loadData();
93
+
94
+ // Export mutable arrays
95
+ export const users: User[] = data.users;
96
+ export const roles: Role[] = data.roles;
97
+ export const permissions: Permission[] = data.permissions;
98
+ export const user_roles: UserRole[] = data.user_roles;
99
+ export const role_permissions: RolePermission[] = data.role_permissions;
100
+ export const user_permissions: UserPermission[] = data.user_permissions;
101
+
102
+ // Helper to save data
103
+ export function saveStore() {
104
+ const payload = {
105
+ users,
106
+ roles,
107
+ permissions,
108
+ user_roles,
109
+ role_permissions,
110
+ user_permissions,
111
+ };
112
+ fs.writeFileSync(dbPath, JSON.stringify(payload, null, 2), "utf-8");
113
+ }
114
+
115
+ // Helper to generate IDs
116
+ export const generateId = () => Math.random().toString(36).substr(2, 9);
@@ -0,0 +1,63 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import jwt from "jsonwebtoken";
3
+ import { sendError } from "../utils/response";
4
+ // Note: We should ideally avoid importing from controllers in middleware
5
+ // But for now we'll keep it to maintain functionality, but point to src if needed
6
+ // However, authController is in src (user land) or lib?
7
+ // Wait, authController was NOT moved to lib. It is in src/controllers.
8
+ // So this import will fail if we use relative paths.
9
+ // But we are in lib.
10
+ // We should probably move ACCESS_TOKEN_EXPIRES_IN_SECONDS to a config or constants file in lib.
11
+ const ACCESS_TOKEN_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
12
+
13
+ export function requireAuth(req: Request, res: Response, next: NextFunction) {
14
+ const header = req.headers.authorization;
15
+ if (!header || !header.startsWith("Bearer ")) {
16
+ sendError(res, 401, "Unauthorized");
17
+ return;
18
+ }
19
+ const token = header.slice(7);
20
+ const secret = process.env.JWT_SECRET;
21
+ if (!secret) {
22
+ sendError(res, 500, "Server misconfigured");
23
+ return;
24
+ }
25
+ try {
26
+ const payload = jwt.verify(token, secret) as {
27
+ userId: string;
28
+ role: string;
29
+ };
30
+ (req as any).user = { userId: payload.userId, role: payload.role };
31
+
32
+ const accessExpiresInSeconds = ACCESS_TOKEN_EXPIRES_IN_SECONDS;
33
+ const accessExpiresAt = new Date(
34
+ Date.now() + accessExpiresInSeconds * 1000
35
+ ).toISOString();
36
+ const newToken = jwt.sign(
37
+ { userId: payload.userId, role: payload.role },
38
+ secret,
39
+ { expiresIn: accessExpiresInSeconds }
40
+ );
41
+ res.setHeader("x-access-token", newToken);
42
+ res.setHeader("x-access-expires-at", accessExpiresAt);
43
+
44
+ next();
45
+ } catch (err: any) {
46
+ sendError(res, 401, "Invalid token");
47
+ }
48
+ }
49
+
50
+ export function requireAdmin(req: Request, res: Response, next: NextFunction) {
51
+ const user = (req as any).user as
52
+ | { userId: string; role: string }
53
+ | undefined;
54
+ if (!user) {
55
+ sendError(res, 401, "Unauthorized");
56
+ return;
57
+ }
58
+ if (user.role !== "admin" && user.role !== "super_admin") {
59
+ sendError(res, 403, "Forbidden");
60
+ return;
61
+ }
62
+ next();
63
+ }