my-crud-lib 1.0.3 → 2.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -131
  3. package/RELEASE.md +32 -0
  4. package/dist/adapter-prisma.d.ts +1 -0
  5. package/dist/adapter-prisma.js +1 -0
  6. package/dist/auth.d.ts +6 -0
  7. package/dist/auth.js +4 -0
  8. package/dist/config/env.d.ts +3 -2
  9. package/dist/config/env.js +9 -3
  10. package/dist/dev.js +1 -1
  11. package/dist/index.d.ts +24 -2
  12. package/dist/index.js +30 -7
  13. package/dist/middleware/hasRole.js +1 -2
  14. package/dist/middleware/isAuth.d.ts +1 -1
  15. package/dist/middleware.d.ts +2 -0
  16. package/dist/middleware.js +2 -0
  17. package/dist/modules/auth/auth.controller.d.ts +2 -1
  18. package/dist/modules/auth/auth.controller.js +11 -22
  19. package/dist/modules/auth/auth.defaults.d.ts +3 -0
  20. package/dist/modules/auth/auth.defaults.js +4 -0
  21. package/dist/modules/auth/auth.service.d.ts +8 -24
  22. package/dist/modules/auth/auth.service.js +54 -32
  23. package/dist/modules/auth/auth.types.d.ts +16 -1
  24. package/dist/modules/user/user.schemas.d.ts +5 -5
  25. package/dist/schemas.d.ts +2 -0
  26. package/dist/schemas.js +2 -0
  27. package/dist/user.d.ts +5 -0
  28. package/dist/user.js +3 -0
  29. package/dist/utils/jwt.d.ts +1 -1
  30. package/dist/utils/jwt.js +4 -4
  31. package/examples/custom-repo/README.md +5 -0
  32. package/examples/custom-repo/server.ts +70 -0
  33. package/examples/express-prisma/.env.example +6 -0
  34. package/examples/express-prisma/README.md +25 -0
  35. package/examples/express-prisma/package.json +23 -0
  36. package/examples/express-prisma/prisma/schema.prisma +32 -0
  37. package/examples/express-prisma/src/server.ts +25 -0
  38. package/package.json +52 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riccardo Sensi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,214 +1,269 @@
1
1
  # my-crud-lib
2
2
 
3
- A modular, TypeScript-first **Auth + User/Profile CRUD** library for Node.js, designed to be **framework-light**, **DB-agnostic** (via adapters), and **highly extensible** (schemas + hooks). Ship a secure `/auth/register`, `/auth/login`, and `/me` in minutes—then customize without forking the core.
3
+ [![CI](https://github.com/riccardosensi99/CRUD-lib/actions/workflows/ci.yml/badge.svg)](https://github.com/riccardosensi99/CRUD-lib/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/my-crud-lib.svg)](https://www.npmjs.com/package/my-crud-lib)
4
5
 
5
- > Works great with Express and Prisma out of the box, but you can plug in your own repo adapter.
6
+ TypeScript-first auth and user/profile CRUD helpers for Node.js and Express.
6
7
 
7
- ---
8
+ The package currently provides:
8
9
 
9
- ## Features
10
+ - Express routers for auth and user CRUD.
11
+ - JWT access and refresh token helpers.
12
+ - Zod schemas for request validation.
13
+ - A `UserRepo` port plus a Prisma adapter.
14
+ - Convenience setup helpers for small Express APIs.
10
15
 
11
- - ✅ Ready-made routes: `POST /auth/register`, `POST /auth/login`, `GET /me`
12
- - 🔐 JWT-based auth with pluggable lifecycle hooks (before/after create, before issuing JWT, etc.)
13
- - 🧩 Extensible validation via **Zod**: merge your own fields into the base schemas
14
- - 🗄️ Repository interfaces (DB-agnostic) + optional Prisma adapter
15
- - 🧰 Cleanly separated core logic & web router
16
- - 🧪 TypeScript types exported for DX
16
+ ## Installation
17
17
 
18
- ---
18
+ ```bash
19
+ npm i my-crud-lib express cors body-parser
20
+ ```
19
21
 
20
- ## Installation
22
+ If you use the bundled Prisma adapter:
21
23
 
22
24
  ```bash
23
- npm i my-crud-lib zod jsonwebtoken bcryptjs
24
- # If using Prisma adapter in your app:
25
- npm i @prisma/client
25
+ npm i @prisma/client prisma
26
+ npx prisma generate
26
27
  ```
27
28
 
28
- > Node.js `>= 18.17` is required.
29
+ Node.js `>=18.17` is required.
29
30
 
30
- ---
31
+ ## Environment
31
32
 
32
- ## Quickstart (Express)
33
+ ```bash
34
+ DATABASE_URL="postgresql://user:password@localhost:5432/app"
35
+ JWT_SECRET="replace-with-a-long-random-secret"
36
+ JWT_ACCESS_EXPIRES_IN="15m"
37
+ JWT_REFRESH_EXPIRES_IN="7d"
38
+ BCRYPT_SALT="10"
39
+ ```
40
+
41
+ `JWT_ACCESS_EXPIRES_IN` and `JWT_REFRESH_EXPIRES_IN` have defaults. `JWT_SECRET` and `DATABASE_URL` must be set before using the default auth and Prisma paths.
42
+
43
+ ## Quickstart With Express And Prisma
33
44
 
34
45
  ```ts
35
- import express from "express";
36
- import { json } from "body-parser";
37
- import { createLibrary } from "my-crud-lib";
38
- // Optional: Prisma adapter (provided in your app)
39
46
  import { PrismaClient } from "@prisma/client";
40
- import { makePrismaUserRepo } from "my-crud-lib/adapter-prisma"; // if you expose this path
47
+ import { createLibrary, createServer } from "my-crud-lib";
48
+ import { makePrismaUserRepo } from "my-crud-lib/adapter-prisma";
41
49
 
42
50
  const prisma = new PrismaClient();
43
-
44
- const app = express();
45
- app.use(json());
51
+ const app = createServer();
46
52
 
47
53
  const lib = createLibrary(
48
54
  {
55
+ routesPrefix: "/api",
49
56
  auth: {
50
- jwtSecret: process.env.JWT_SECRET!, // e.g. "supersecret"
51
- jwtExpiresIn: "7d",
52
57
  passwordHashRounds: 10,
53
58
  },
54
- routesPrefix: "/api", // optional
55
59
  },
56
60
  { userRepo: makePrismaUserRepo(prisma) }
57
61
  );
58
62
 
59
63
  app.use(lib.router);
60
64
 
61
- app.listen(3000, () => console.log("API running on http://localhost:3000"));
65
+ app.listen(3000, () => {
66
+ console.log("API running on http://localhost:3000");
67
+ });
62
68
  ```
63
69
 
64
- ### Available Routes
65
-
66
- - `POST /auth/register` → create user (email + password + optional name)
67
- - `POST /auth/login` → returns `{ accessToken }`
68
- - `GET /me` → authenticated endpoint, returns the current user
70
+ With the `/api` prefix, the mounted routes include:
69
71
 
70
- > Protect `/me` with the `isAuth` middleware already wired inside the library router.
72
+ - `POST /api/auth/register`
73
+ - `POST /api/auth/login`
74
+ - `POST /api/auth/refresh`
75
+ - `GET /api/auth/me`
76
+ - `GET /api/users`
77
+ - `GET /api/users/me`
78
+ - `PUT /api/users/me`
79
+ - `POST /api/users`
80
+ - `GET /api/users/:id`
81
+ - `PUT /api/users/:id`
82
+ - `DELETE /api/users/:id`
71
83
 
72
- ---
84
+ Admin user routes require a bearer token with role `ADMIN`.
73
85
 
74
- ## Configuration
86
+ ## Examples
75
87
 
76
- ```ts
77
- type AuthConfig = {
78
- jwtSecret: string;
79
- jwtExpiresIn: string; // e.g. "7d"
80
- passwordHashRounds: number; // e.g. 10
81
- };
82
-
83
- type LibraryConfig = {
84
- auth: AuthConfig;
85
- routesPrefix?: string; // e.g. "/api"
86
- };
87
- ```
88
+ - `examples/express-prisma` is a runnable Express + Prisma app.
89
+ - `examples/custom-repo` shows the `UserRepo` shape with an in-memory adapter.
88
90
 
89
- Create the library:
91
+ Run the Prisma example:
90
92
 
91
- ```ts
92
- const lib = createLibrary(config, { userRepo });
93
+ ```bash
94
+ cd examples/express-prisma
95
+ npm install
96
+ cp .env.example .env
97
+ npx prisma generate
98
+ npx prisma migrate dev --name init
99
+ npm run dev
93
100
  ```
94
101
 
95
- ---
96
-
97
- ## Extending Schemas (Zod)
102
+ ## Response Examples
98
103
 
99
- The library exports base Zod schemas and a factory to merge your custom fields.
104
+ Register:
100
105
 
101
- ```ts
102
- // consumer app
103
- import { z } from "zod";
104
- import { makeCreateUserSchema } from "my-crud-lib/schemas";
106
+ ```http
107
+ POST /api/auth/register
108
+ Content-Type: application/json
105
109
 
106
- const ExtraUserFields = z.object({
107
- companyVat: z.string().min(5),
108
- marketingOptIn: z.boolean().default(false),
109
- });
110
+ {
111
+ "email": "reader@example.com",
112
+ "password": "password123",
113
+ "name": "Reader"
114
+ }
115
+ ```
110
116
 
111
- export const CreateUserSchema = makeCreateUserSchema(ExtraUserFields);
117
+ Response:
112
118
 
113
- // Later in your route (if you override the built-in):
114
- const data = CreateUserSchema.parse(req.body);
119
+ ```json
120
+ {
121
+ "user": {
122
+ "id": 1,
123
+ "email": "reader@example.com",
124
+ "name": "Reader",
125
+ "role": "USER"
126
+ },
127
+ "accessToken": "eyJ...",
128
+ "refreshToken": "eyJ..."
129
+ }
115
130
  ```
116
131
 
117
- **Tip:** The default Prisma schema (if you use it) exposes `profile.extra: Json?` so you can store arbitrary fields without altering the core tables.
118
-
119
- ---
132
+ Login:
120
133
 
121
- ## Hooks (Lifecycle)
134
+ ```http
135
+ POST /api/auth/login
136
+ Content-Type: application/json
122
137
 
123
- Use hooks to change data or enrich tokens without forking.
138
+ {
139
+ "email": "reader@example.com",
140
+ "password": "password123"
141
+ }
142
+ ```
124
143
 
125
- ```ts
126
- import { plugins } from "my-crud-lib";
144
+ Protected request:
127
145
 
128
- plugins.use({
129
- beforeCreateUser: async (data, ctx) => {
130
- if (data.companyVat) data.companyVat = data.companyVat.toUpperCase();
131
- return data;
132
- },
133
- afterCreateUser: async (user, ctx) => {
134
- // e.g., send welcome email or audit log
135
- },
136
- beforeIssueJwt: (payload, ctx) => {
137
- return { ...payload, tenantId: "acme-123" };
138
- },
139
- });
146
+ ```http
147
+ GET /api/auth/me
148
+ Authorization: Bearer <accessToken>
140
149
  ```
141
150
 
142
- **Available hooks**
143
- - `beforeCreateUser(data, ctx)`
144
- - `afterCreateUser(user, ctx)`
145
- - `beforeUpdateUser(data, ctx)`
146
- - `beforeIssueJwt(payload, ctx)`
151
+ ## Public Imports
147
152
 
148
- `ctx` includes the request and useful dependencies (e.g., repos).
149
-
150
- ---
153
+ ```ts
154
+ import {
155
+ createLibrary,
156
+ createServer,
157
+ mountDefaultRoutes,
158
+ createAuthRouter,
159
+ createUserRouter,
160
+ isAuth,
161
+ hasRole,
162
+ } from "my-crud-lib";
163
+
164
+ import { createAuthRouter, makeAuthService, registerSchema, loginSchema } from "my-crud-lib/auth";
165
+ import { createUserRouter, type UserRepo } from "my-crud-lib/user";
166
+ import { registerSchema, listUsersQuerySchema } from "my-crud-lib/schemas";
167
+ import { isAuth, hasRole } from "my-crud-lib/middleware";
168
+ import { makePrismaUserRepo } from "my-crud-lib/adapter-prisma";
169
+ import { makePrismaUserRepo as makePrismaUserRepoCanonical } from "my-crud-lib/adapters/prisma";
170
+ ```
151
171
 
152
- ## Repository Adapters (DB-agnostic)
172
+ ## Repository Adapter
153
173
 
154
- Core interface:
174
+ User CRUD is driven by the `UserRepo` interface:
155
175
 
156
176
  ```ts
157
177
  export interface UserRepo {
158
- create(data: any): Promise<any>;
159
- update(id: string, data: any): Promise<any>;
160
- findById(id: string): Promise<any | null>;
161
- findByEmail(email: string): Promise<any | null>;
178
+ count(where: { role?: string; search?: string }): Promise<number>;
179
+ findMany(params: {
180
+ page: number;
181
+ pageSize: number;
182
+ role?: string;
183
+ search?: string;
184
+ sortField: "createdAt" | "updatedAt" | "email" | "name";
185
+ sortDir: "asc" | "desc";
186
+ }): Promise<UserListItem[]>;
187
+ findById(id: number | string): Promise<UserListItem | null>;
188
+ findByEmail(email: string): Promise<(UserListItem & { passwordHash?: string }) | null>;
189
+ create(input: {
190
+ email: string;
191
+ passwordHash: string;
192
+ name?: string | null;
193
+ role?: string;
194
+ bio?: string | null;
195
+ avatarUrl?: string | null;
196
+ }): Promise<UserListItem>;
197
+ update(id: number | string, input: AdminUpdateUserInput): Promise<UserListItem>;
198
+ delete(id: number | string): Promise<void>;
199
+ updateMe(
200
+ userId: number | string,
201
+ input: { name?: string | null; bio?: string | null; avatarUrl?: string | null }
202
+ ): Promise<UserListItem>;
162
203
  }
163
204
  ```
164
205
 
165
- Example Prisma adapter (in your app or provided by the lib):
206
+ The Prisma adapter is available from both import paths:
166
207
 
167
208
  ```ts
168
- export function makePrismaUserRepo(prisma: any): UserRepo {
169
- return {
170
- create: (data) => prisma.user.create({ data }),
171
- update: (id, data) => prisma.user.update({ where: { id }, data }),
172
- findById: (id) => prisma.user.findUnique({ where: { id } }),
173
- findByEmail: (email) => prisma.user.findUnique({ where: { email } }),
174
- };
175
- }
209
+ import { makePrismaUserRepo } from "my-crud-lib/adapter-prisma";
210
+ // or
211
+ import { makePrismaUserRepo } from "my-crud-lib/adapters/prisma";
212
+ ```
213
+
214
+ Auth also receives the same repository dependency:
215
+
216
+ ```ts
217
+ import { createAuthRouter } from "my-crud-lib/auth";
218
+
219
+ app.use("/auth", createAuthRouter({ userRepo }));
176
220
  ```
177
221
 
178
- ---
222
+ ## Build Checks
179
223
 
180
- ## Types
224
+ ```bash
225
+ npm run build
226
+ npm run smoke:exports
227
+ npm run smoke:auth-hardening
228
+ npm run smoke:auth-service
229
+ ```
181
230
 
182
- The package exports the main public types:
231
+ `smoke:exports` builds the package and imports the documented public paths from `dist`.
232
+ `smoke:auth-hardening` checks auth safety defaults and JWT secret validation.
233
+ `smoke:auth-service` verifies register/login/refresh/me with an in-memory repo.
183
234
 
184
- - `LibraryConfig`, `AuthConfig`
185
- - `UserRepo`
186
- - schema types (e.g., `CreateUserBase`)
235
+ ## Current Limitations
187
236
 
188
- ---
237
+ - Lifecycle hooks and schema factories are not part of the current public API.
238
+ - The Prisma schema is included as a starter schema; consumer apps should own their migrations.
189
239
 
190
- ## Security Notes
240
+ ## Troubleshooting
191
241
 
192
- - Keep `JWT_SECRET` secure; rotate if compromised.
193
- - Consider adding rate limiting in your app (e.g., `express-rate-limit`).
194
- - Store password hashes using `bcryptjs` with adequate rounds (default shown: `10`).
195
- - Use HTTPS in production.
242
+ `Cannot find module '@prisma/client'`
196
243
 
197
- ---
244
+ Install Prisma dependencies in your app and run `npx prisma generate`.
198
245
 
199
- ## Optional
246
+ `JWT_SECRET is required before signing or verifying tokens`
200
247
 
201
- if you want to use the prisma adeapter use:
248
+ Set `JWT_SECRET` before mounting or calling auth routes. Use a long random value.
202
249
 
203
- npm i @prisma/client prisma
204
- npx prisma generate
250
+ `Invalid or expired token`
251
+
252
+ Send the access token in the `Authorization` header as `Bearer <accessToken>`. Use `/auth/refresh` with a refresh token to get a new pair.
205
253
 
206
- ## Contributing
254
+ ESM import errors
207
255
 
208
- PRs and issues are welcome! Please follow conventional commits or include a clear description. For releases, we recommend Changesets or semantic-release.
256
+ Use Node.js `>=18.17` and import from the documented package paths, for example `my-crud-lib`, `my-crud-lib/auth`, or `my-crud-lib/adapter-prisma`.
209
257
 
210
- ---
258
+ ## Security Notes
259
+
260
+ - Use a long random `JWT_SECRET` and rotate it if compromised.
261
+ - Keep access tokens short-lived.
262
+ - Add rate limiting around auth endpoints in production.
263
+ - Use HTTPS in production.
264
+ - Self-registration creates `USER` accounts by default.
265
+ - Create `ADMIN` accounts intentionally through your own seed/admin workflow.
211
266
 
212
267
  ## License
213
268
 
214
- MIT © Riccardo
269
+ MIT
package/RELEASE.md ADDED
@@ -0,0 +1,32 @@
1
+ # Release Checklist
2
+
3
+ Use this checklist before publishing `my-crud-lib` to npm.
4
+
5
+ ## Preflight
6
+
7
+ - Confirm `package.json` version is the intended release version.
8
+ - Confirm `README.md` examples match tested public imports.
9
+ - Confirm `LICENSE`, `README.md`, `examples`, `dist`, and `prisma/schema.prisma` are included in the package.
10
+ - Review open issues for release blockers.
11
+
12
+ ## Verify
13
+
14
+ ```bash
15
+ npm ci
16
+ npm run ci
17
+ npm pack --dry-run
18
+ ```
19
+
20
+ Inspect the dry-run file list before publishing.
21
+
22
+ ## Publish
23
+
24
+ ```bash
25
+ npm publish --access public
26
+ ```
27
+
28
+ ## After Publish
29
+
30
+ - Create a GitHub release or tag for the published version.
31
+ - Confirm the npm page shows repository, license, README, examples, and keywords.
32
+ - Smoke test installation in a fresh temporary project.
@@ -0,0 +1 @@
1
+ export { makePrismaUserRepo } from './adapters/prisma.js';
@@ -0,0 +1 @@
1
+ export { makePrismaUserRepo } from './adapters/prisma.js';
package/dist/auth.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { createAuthRouter } from './modules/auth/auth.controller.js';
2
+ export { DEFAULT_REGISTER_ROLE, resolveRegisterRole } from './modules/auth/auth.defaults.js';
3
+ export { makeAuthService } from './modules/auth/auth.service.js';
4
+ export { loginSchema, registerSchema } from './modules/auth/auth.schemas.js';
5
+ export type { AuthResult, AuthServiceDeps, AuthTokens, AuthUser, AuthUserRepo } from './modules/auth/auth.types.js';
6
+ export type { LoginInput, RegisterInput } from './modules/auth/auth.schemas.js';
package/dist/auth.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createAuthRouter } from './modules/auth/auth.controller.js';
2
+ export { DEFAULT_REGISTER_ROLE, resolveRegisterRole } from './modules/auth/auth.defaults.js';
3
+ export { makeAuthService } from './modules/auth/auth.service.js';
4
+ export { loginSchema, registerSchema } from './modules/auth/auth.schemas.js';
@@ -1,2 +1,3 @@
1
- export declare const JWT_ACCESS_EXPIRES_IN: string, JWT_REFRESH_EXPIRES_IN: string;
2
- export declare const JWT_SECRET: string;
1
+ export declare const JWT_ACCESS_EXPIRES_IN: string;
2
+ export declare const JWT_REFRESH_EXPIRES_IN: string;
3
+ export declare function getJwtSecret(): string;
@@ -1,5 +1,11 @@
1
1
  import dotenv from "dotenv";
2
- import { env } from "process";
3
2
  dotenv.config();
4
- export const { JWT_ACCESS_EXPIRES_IN = env.JWT_ACCESS_EXPIRES_IN || "15m", JWT_REFRESH_EXPIRES_IN = env.JWT_REFRESH_EXPIRES_IN || "7d", } = process.env;
5
- export const JWT_SECRET = process.env.JWT_SECRET;
3
+ export const JWT_ACCESS_EXPIRES_IN = process.env.JWT_ACCESS_EXPIRES_IN || "15m";
4
+ export const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || "7d";
5
+ export function getJwtSecret() {
6
+ const secret = process.env.JWT_SECRET?.trim();
7
+ if (!secret) {
8
+ throw new Error("JWT_SECRET is required before signing or verifying tokens");
9
+ }
10
+ return secret;
11
+ }
package/dist/dev.js CHANGED
@@ -7,7 +7,7 @@ import { PrismaClient } from '@prisma/client';
7
7
  const app = createServer();
8
8
  const prisma = new PrismaClient();
9
9
  const userRepo = makePrismaUserRepo(prisma);
10
- app.use('/auth', createAuthRouter());
10
+ app.use('/auth', createAuthRouter({ userRepo }));
11
11
  app.use('/users', createUserRouter({ userRepo }));
12
12
  const PORT = Number(process.env.PORT) || 3000;
13
13
  app.listen(PORT, () => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,27 @@
1
1
  import { type Express } from 'express';
2
- export { createAuthRouter } from './modules/auth/auth.controller.js';
2
+ export type { UserRepo } from './core/ports/user.repo.js';
3
+ export type { AdminCreateUserInput, AdminUpdateUserInput, ListUsersQuery, Paginated, Role, UpdateMeInput, UserListItem, } from './modules/user/user.types.js';
3
4
  export { createUserRouter } from './modules/user/user.controller.js';
5
+ export { createAuthRouter } from './modules/auth/auth.controller.js';
6
+ export { DEFAULT_REGISTER_ROLE, resolveRegisterRole } from './modules/auth/auth.defaults.js';
7
+ export { makeAuthService } from './modules/auth/auth.service.js';
8
+ export { registerSchema, loginSchema } from './modules/auth/auth.schemas.js';
9
+ export type { AuthResult, AuthServiceDeps, AuthTokens, AuthUser, AuthUserRepo } from './modules/auth/auth.types.js';
10
+ export { SortEnum, adminCreateUserSchema, adminUpdateUserSchema, listUsersQuerySchema, updateMeSchema, } from './modules/user/user.schemas.js';
11
+ export { isAuth, type AuthRequest } from './middleware/isAuth.js';
12
+ export { hasRole, isSelfOrAdmin } from './middleware/hasRole.js';
13
+ export { makePrismaUserRepo } from './adapters/prisma.js';
14
+ import type { UserRepo } from './core/ports/user.repo.js';
15
+ import type { AuthServiceDeps } from './modules/auth/auth.types.js';
16
+ export type LibraryConfig = {
17
+ routesPrefix?: string;
18
+ auth?: Omit<AuthServiceDeps, 'userRepo'>;
19
+ };
20
+ export type LibraryDeps = {
21
+ userRepo: UserRepo;
22
+ };
4
23
  export declare function createServer(): Express;
5
- export declare function mountDefaultRoutes(app: Express): void;
24
+ export declare function createLibrary(config: LibraryConfig, deps: LibraryDeps): {
25
+ router: import("express-serve-static-core").Router;
26
+ };
27
+ export declare function mountDefaultRoutes(app: Express, deps: LibraryDeps, config?: LibraryConfig): void;
package/dist/index.js CHANGED
@@ -1,17 +1,40 @@
1
- import express from 'express';
1
+ import express, { Router } from 'express';
2
2
  import cors from 'cors';
3
3
  import bodyParser from 'body-parser';
4
- export { createAuthRouter } from './modules/auth/auth.controller.js';
5
4
  export { createUserRouter } from './modules/user/user.controller.js';
5
+ export { createAuthRouter } from './modules/auth/auth.controller.js';
6
+ export { DEFAULT_REGISTER_ROLE, resolveRegisterRole } from './modules/auth/auth.defaults.js';
7
+ export { makeAuthService } from './modules/auth/auth.service.js';
8
+ export { registerSchema, loginSchema } from './modules/auth/auth.schemas.js';
9
+ export { SortEnum, adminCreateUserSchema, adminUpdateUserSchema, listUsersQuerySchema, updateMeSchema, } from './modules/user/user.schemas.js';
10
+ export { isAuth } from './middleware/isAuth.js';
11
+ export { hasRole, isSelfOrAdmin } from './middleware/hasRole.js';
12
+ export { makePrismaUserRepo } from './adapters/prisma.js';
13
+ import { createAuthRouter } from './modules/auth/auth.controller.js';
14
+ import { createUserRouter } from './modules/user/user.controller.js';
15
+ function normalizePrefix(prefix) {
16
+ if (!prefix)
17
+ return '';
18
+ const trimmed = prefix.trim();
19
+ if (!trimmed || trimmed === '/')
20
+ return '';
21
+ const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
22
+ return withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
23
+ }
6
24
  export function createServer() {
7
25
  const app = express();
8
26
  app.use(cors());
9
27
  app.use(bodyParser.json());
10
28
  return app;
11
29
  }
12
- export function mountDefaultRoutes(app) {
13
- const { createAuthRouter } = require('./modules/auth/auth.controller.js');
14
- const { createUserRouter } = require('./modules/user/user.controller.js');
15
- app.use('/auth', createAuthRouter());
16
- app.use('/users', createUserRouter());
30
+ export function createLibrary(config, deps) {
31
+ const router = Router();
32
+ const prefix = normalizePrefix(config.routesPrefix);
33
+ router.use(`${prefix}/auth`, createAuthRouter({ userRepo: deps.userRepo, ...config.auth }));
34
+ router.use(`${prefix}/users`, createUserRouter({ userRepo: deps.userRepo }));
35
+ return { router };
36
+ }
37
+ export function mountDefaultRoutes(app, deps, config = {}) {
38
+ const { router } = createLibrary(config, deps);
39
+ app.use(router);
17
40
  }
@@ -11,8 +11,7 @@ export function isSelfOrAdmin() {
11
11
  return (req, res, next) => {
12
12
  const uid = req.user?.id;
13
13
  const role = req.user?.role;
14
- const paramId = Number(req.params.id);
15
- if (role === 'ADMIN' || uid === paramId)
14
+ if (role === 'ADMIN' || String(uid) === req.params.id)
16
15
  return next();
17
16
  return res.status(403).json({ error: 'Forbidden' });
18
17
  };
@@ -1,7 +1,7 @@
1
1
  import type { Request, Response, NextFunction } from 'express';
2
2
  export type AuthRequest = Request & {
3
3
  user?: {
4
- id: number;
4
+ id: number | string;
5
5
  role: 'USER' | 'ADMIN';
6
6
  };
7
7
  };
@@ -0,0 +1,2 @@
1
+ export { hasRole, isSelfOrAdmin } from './middleware/hasRole.js';
2
+ export { isAuth, type AuthRequest } from './middleware/isAuth.js';
@@ -0,0 +1,2 @@
1
+ export { hasRole, isSelfOrAdmin } from './middleware/hasRole.js';
2
+ export { isAuth } from './middleware/isAuth.js';
@@ -1 +1,2 @@
1
- export declare function createAuthRouter(): import("express-serve-static-core").Router;
1
+ import type { AuthServiceDeps } from './auth.types.js';
2
+ export declare function createAuthRouter(deps: AuthServiceDeps): import("express-serve-static-core").Router;
@@ -1,20 +1,19 @@
1
1
  import { Router } from 'express';
2
- import { registerSchema, loginSchema } from './auth.schemas.js';
3
- import { registerUser, loginUser } from './auth.service.js';
4
- import { verifyToken, signAccessToken, signRefreshToken } from '../../utils/jwt.js';
5
2
  import { isAuth } from '../../middleware/isAuth.js';
6
- import { prisma } from '../../utils/prisma.js';
7
- export function createAuthRouter() {
3
+ import { loginSchema, registerSchema } from './auth.schemas.js';
4
+ import { makeAuthService } from './auth.service.js';
5
+ export function createAuthRouter(deps) {
8
6
  const router = Router();
7
+ const service = makeAuthService(deps);
9
8
  router.post('/register', async (req, res) => {
10
9
  try {
11
10
  const data = registerSchema.parse(req.body);
12
- const result = await registerUser(data);
11
+ const result = await service.registerUser(data);
13
12
  return res.status(201).json(result);
14
13
  }
15
14
  catch (err) {
16
15
  if (err?.message === 'EMAIL_TAKEN')
17
- return res.status(409).json({ error: 'Email già registrata' });
16
+ return res.status(409).json({ error: 'Email already registered' });
18
17
  if (err?.issues)
19
18
  return res.status(400).json({ error: 'ValidationError', details: err.issues });
20
19
  return res.status(500).json({ error: 'InternalError' });
@@ -23,12 +22,12 @@ export function createAuthRouter() {
23
22
  router.post('/login', async (req, res) => {
24
23
  try {
25
24
  const data = loginSchema.parse(req.body);
26
- const result = await loginUser(data);
25
+ const result = await service.loginUser(data);
27
26
  return res.json(result);
28
27
  }
29
28
  catch (err) {
30
29
  if (err?.message === 'INVALID_CREDENTIALS')
31
- return res.status(401).json({ error: 'Credenziali non valide' });
30
+ return res.status(401).json({ error: 'Invalid credentials' });
32
31
  if (err?.issues)
33
32
  return res.status(400).json({ error: 'ValidationError', details: err.issues });
34
33
  return res.status(500).json({ error: 'InternalError' });
@@ -39,15 +38,8 @@ export function createAuthRouter() {
39
38
  const { refreshToken } = req.body;
40
39
  if (!refreshToken)
41
40
  return res.status(400).json({ error: 'Missing refreshToken' });
42
- const payload = verifyToken(refreshToken);
43
- if (payload.typ !== 'refresh')
44
- return res.status(401).json({ error: 'Invalid refresh token' });
45
- const user = await prisma.user.findUnique({ where: { id: payload.sub }, select: { id: true, role: true } });
46
- if (!user)
47
- return res.status(401).json({ error: 'User not found' });
48
- const accessToken = signAccessToken({ sub: user.id, role: user.role });
49
- const newRefreshToken = signRefreshToken({ sub: user.id, role: user.role });
50
- return res.json({ accessToken, refreshToken: newRefreshToken });
41
+ const tokens = await service.refreshSession(refreshToken);
42
+ return res.json(tokens);
51
43
  }
52
44
  catch {
53
45
  return res.status(401).json({ error: 'Invalid or expired refresh token' });
@@ -55,10 +47,7 @@ export function createAuthRouter() {
55
47
  });
56
48
  router.get('/me', isAuth, async (req, res) => {
57
49
  const userId = req.user.id;
58
- const me = await prisma.user.findUnique({
59
- where: { id: userId },
60
- select: { id: true, email: true, name: true, role: true, profile: { select: { bio: true, avatarUrl: true } } },
61
- });
50
+ const me = await service.getMe(userId);
62
51
  if (!me)
63
52
  return res.status(404).json({ error: 'User not found' });
64
53
  return res.json(me);
@@ -0,0 +1,3 @@
1
+ import type { Role } from '../user/user.types.js';
2
+ export declare const DEFAULT_REGISTER_ROLE: Role;
3
+ export declare function resolveRegisterRole(role?: Role): Role;
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_REGISTER_ROLE = 'USER';
2
+ export function resolveRegisterRole(role) {
3
+ return role ?? DEFAULT_REGISTER_ROLE;
4
+ }
@@ -1,24 +1,8 @@
1
- import type { RegisterInput } from './auth.schemas.js';
2
- export declare function registerUser(params: RegisterInput): Promise<{
3
- user: {
4
- email: string;
5
- name: string | null;
6
- id: number;
7
- role: import("@prisma/client").$Enums.Role;
8
- };
9
- accessToken: string;
10
- refreshToken: string;
11
- }>;
12
- export declare function loginUser(params: {
13
- email: string;
14
- password: string;
15
- }): Promise<{
16
- user: {
17
- id: number;
18
- email: string;
19
- name: string | null;
20
- role: import("@prisma/client").$Enums.Role;
21
- };
22
- accessToken: string;
23
- refreshToken: string;
24
- }>;
1
+ import type { LoginInput, RegisterInput } from './auth.schemas.js';
2
+ import type { AuthResult, AuthServiceDeps, AuthTokens, AuthUser } from './auth.types.js';
3
+ export declare function makeAuthService(deps: AuthServiceDeps): {
4
+ registerUser(params: RegisterInput): Promise<AuthResult>;
5
+ loginUser(params: LoginInput): Promise<AuthResult>;
6
+ refreshSession(refreshToken: string): Promise<AuthTokens>;
7
+ getMe(userId: number | string): Promise<AuthUser | null>;
8
+ };
@@ -1,38 +1,60 @@
1
- import { prisma } from '../../utils/prisma.js';
2
1
  import bcrypt from 'bcryptjs';
3
- import { signAccessToken, signRefreshToken } from '../../utils/jwt.js';
4
- export async function registerUser(params) {
5
- const exists = await prisma.user.findUnique({ where: { email: params.email } });
6
- if (exists)
7
- throw new Error('EMAIL_TAKEN');
8
- const passwordHash = await bcrypt.hash(params.password, Number(process.env.BCRYPT_SALT) || 10);
9
- const user = await prisma.user.create({
10
- data: {
11
- email: params.email,
12
- passwordHash,
13
- name: params.name ?? null,
14
- role: 'ADMIN',
15
- profile: { create: {} },
16
- },
17
- select: { id: true, email: true, name: true, role: true },
18
- });
19
- const accessToken = signAccessToken({ sub: user.id, role: user.role });
20
- const refreshToken = signRefreshToken({ sub: user.id, role: user.role });
21
- return { user, accessToken, refreshToken };
2
+ import { signAccessToken, signRefreshToken, verifyToken } from '../../utils/jwt.js';
3
+ import { resolveRegisterRole } from './auth.defaults.js';
4
+ function resolvePasswordHashRounds(configured) {
5
+ const rounds = configured ?? Number(process.env.BCRYPT_SALT);
6
+ return Number.isInteger(rounds) && rounds > 0 ? rounds : 10;
7
+ }
8
+ function toAuthUser(user) {
9
+ const { passwordHash: _passwordHash, ...safeUser } = user;
10
+ return safeUser;
22
11
  }
23
- export async function loginUser(params) {
24
- const user = await prisma.user.findUnique({ where: { email: params.email } });
25
- if (!user)
26
- throw new Error('INVALID_CREDENTIALS');
27
- const ok = await bcrypt.compare(params.password, user.passwordHash);
28
- if (!ok)
29
- throw new Error('INVALID_CREDENTIALS');
12
+ function issueTokens(user) {
30
13
  const payload = { sub: user.id, role: user.role };
31
- const accessToken = signAccessToken(payload);
32
- const refreshToken = signRefreshToken(payload);
33
14
  return {
34
- user: { id: user.id, email: user.email, name: user.name, role: user.role },
35
- accessToken,
36
- refreshToken,
15
+ accessToken: signAccessToken(payload),
16
+ refreshToken: signRefreshToken(payload),
17
+ };
18
+ }
19
+ export function makeAuthService(deps) {
20
+ const { userRepo } = deps;
21
+ return {
22
+ async registerUser(params) {
23
+ const exists = await userRepo.findByEmail(params.email);
24
+ if (exists)
25
+ throw new Error('EMAIL_TAKEN');
26
+ const passwordHash = await bcrypt.hash(params.password, resolvePasswordHashRounds(deps.passwordHashRounds));
27
+ const role = resolveRegisterRole(deps.defaultRegisterRole);
28
+ const user = await userRepo.create({
29
+ email: params.email,
30
+ passwordHash,
31
+ name: params.name ?? null,
32
+ role,
33
+ });
34
+ const authUser = toAuthUser(user);
35
+ return { user: authUser, ...issueTokens(authUser) };
36
+ },
37
+ async loginUser(params) {
38
+ const user = await userRepo.findByEmail(params.email);
39
+ if (!user?.passwordHash)
40
+ throw new Error('INVALID_CREDENTIALS');
41
+ const ok = await bcrypt.compare(params.password, user.passwordHash);
42
+ if (!ok)
43
+ throw new Error('INVALID_CREDENTIALS');
44
+ const authUser = toAuthUser(user);
45
+ return { user: authUser, ...issueTokens(authUser) };
46
+ },
47
+ async refreshSession(refreshToken) {
48
+ const payload = verifyToken(refreshToken);
49
+ if (payload.typ !== 'refresh')
50
+ throw new Error('INVALID_REFRESH_TOKEN');
51
+ const user = await userRepo.findById(payload.sub);
52
+ if (!user)
53
+ throw new Error('USER_NOT_FOUND');
54
+ return issueTokens(user);
55
+ },
56
+ getMe(userId) {
57
+ return userRepo.findById(userId);
58
+ },
37
59
  };
38
60
  }
@@ -1 +1,16 @@
1
- export {};
1
+ import type { UserRepo } from '../../core/ports/user.repo.js';
2
+ import type { Role, UserListItem } from '../user/user.types.js';
3
+ export type AuthUserRepo = Pick<UserRepo, 'create' | 'findByEmail' | 'findById'>;
4
+ export type AuthServiceDeps = {
5
+ userRepo: AuthUserRepo;
6
+ passwordHashRounds?: number;
7
+ defaultRegisterRole?: Role;
8
+ };
9
+ export type AuthUser = UserListItem;
10
+ export type AuthTokens = {
11
+ accessToken: string;
12
+ refreshToken: string;
13
+ };
14
+ export type AuthResult = AuthTokens & {
15
+ user: AuthUser;
16
+ };
@@ -7,17 +7,17 @@ export declare const listUsersQuerySchema: z.ZodObject<{
7
7
  role: z.ZodOptional<z.ZodEnum<["USER", "ADMIN"]>>;
8
8
  sort: z.ZodDefault<z.ZodEnum<["createdAt:desc", "createdAt:asc", "updatedAt:asc", "updatedAt:desc", "email:asc", "email:desc", "name:asc", "name:desc"]>>;
9
9
  }, "strip", z.ZodTypeAny, {
10
- sort: "createdAt:desc" | "createdAt:asc" | "updatedAt:asc" | "updatedAt:desc" | "email:asc" | "email:desc" | "name:asc" | "name:desc";
11
10
  page: number;
12
11
  pageSize: number;
13
- search?: string | undefined;
12
+ sort: "createdAt:asc" | "createdAt:desc" | "updatedAt:asc" | "updatedAt:desc" | "email:asc" | "email:desc" | "name:asc" | "name:desc";
14
13
  role?: "USER" | "ADMIN" | undefined;
15
- }, {
16
- sort?: "createdAt:desc" | "createdAt:asc" | "updatedAt:asc" | "updatedAt:desc" | "email:asc" | "email:desc" | "name:asc" | "name:desc" | undefined;
17
14
  search?: string | undefined;
15
+ }, {
18
16
  role?: "USER" | "ADMIN" | undefined;
17
+ search?: string | undefined;
19
18
  page?: number | undefined;
20
19
  pageSize?: number | undefined;
20
+ sort?: "createdAt:asc" | "createdAt:desc" | "updatedAt:asc" | "updatedAt:desc" | "email:asc" | "email:desc" | "name:asc" | "name:desc" | undefined;
21
21
  }>;
22
22
  export declare const updateMeSchema: z.ZodObject<{
23
23
  name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -41,8 +41,8 @@ export declare const adminCreateUserSchema: z.ZodObject<{
41
41
  avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
42
  }, "strip", z.ZodTypeAny, {
43
43
  email: string;
44
- password: string;
45
44
  role: "USER" | "ADMIN";
45
+ password: string;
46
46
  name?: string | null | undefined;
47
47
  bio?: string | null | undefined;
48
48
  avatarUrl?: string | null | undefined;
@@ -0,0 +1,2 @@
1
+ export { loginSchema, registerSchema } from './modules/auth/auth.schemas.js';
2
+ export { SortEnum, adminCreateUserSchema, adminUpdateUserSchema, listUsersQuerySchema, updateMeSchema, } from './modules/user/user.schemas.js';
@@ -0,0 +1,2 @@
1
+ export { loginSchema, registerSchema } from './modules/auth/auth.schemas.js';
2
+ export { SortEnum, adminCreateUserSchema, adminUpdateUserSchema, listUsersQuerySchema, updateMeSchema, } from './modules/user/user.schemas.js';
package/dist/user.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { createUserRouter } from './modules/user/user.controller.js';
2
+ export { makeUserService } from './modules/user/user.service.js';
3
+ export { SortEnum, adminCreateUserSchema, adminUpdateUserSchema, listUsersQuerySchema, updateMeSchema, } from './modules/user/user.schemas.js';
4
+ export type { AdminCreateUserInput, AdminUpdateUserInput, ListUsersQuery, Paginated, Role, UpdateMeInput, UserListItem, } from './modules/user/user.types.js';
5
+ export type { UserRepo } from './core/ports/user.repo.js';
package/dist/user.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createUserRouter } from './modules/user/user.controller.js';
2
+ export { makeUserService } from './modules/user/user.service.js';
3
+ export { SortEnum, adminCreateUserSchema, adminUpdateUserSchema, listUsersQuerySchema, updateMeSchema, } from './modules/user/user.schemas.js';
@@ -1,5 +1,5 @@
1
1
  export type JwtPayload = {
2
- sub: number;
2
+ sub: number | string;
3
3
  role: "USER" | "ADMIN";
4
4
  typ?: "refresh";
5
5
  };
package/dist/utils/jwt.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import jwt from "jsonwebtoken";
2
- import { JWT_ACCESS_EXPIRES_IN, JWT_REFRESH_EXPIRES_IN, JWT_SECRET } from "../config/env.js";
2
+ import { getJwtSecret, JWT_ACCESS_EXPIRES_IN, JWT_REFRESH_EXPIRES_IN } from "../config/env.js";
3
3
  export function signAccessToken(payload) {
4
- return jwt.sign(payload, JWT_SECRET, {
4
+ return jwt.sign(payload, getJwtSecret(), {
5
5
  expiresIn: JWT_ACCESS_EXPIRES_IN,
6
6
  });
7
7
  }
8
8
  export function signRefreshToken(payload) {
9
- return jwt.sign({ ...payload, typ: "refresh" }, JWT_SECRET, {
9
+ return jwt.sign({ ...payload, typ: "refresh" }, getJwtSecret(), {
10
10
  expiresIn: JWT_REFRESH_EXPIRES_IN,
11
11
  });
12
12
  }
13
13
  export function verifyToken(token) {
14
- return jwt.verify(token, JWT_SECRET);
14
+ return jwt.verify(token, getJwtSecret());
15
15
  }
@@ -0,0 +1,5 @@
1
+ # Custom Repository Example
2
+
3
+ This example shows the shape of an app-owned `UserRepo`.
4
+
5
+ It is intentionally in-memory and only useful for understanding the adapter contract. Use a real database in production.
@@ -0,0 +1,70 @@
1
+ import { createLibrary, createServer, type UserRepo, type UserListItem } from 'my-crud-lib';
2
+
3
+ type StoredUser = UserListItem & { passwordHash: string };
4
+
5
+ const users = new Map<number, StoredUser>();
6
+ let nextId = 1;
7
+
8
+ const publicUser = (user: StoredUser): UserListItem => {
9
+ const { passwordHash: _passwordHash, ...safeUser } = user;
10
+ return safeUser;
11
+ };
12
+
13
+ const userRepo: UserRepo = {
14
+ async count() {
15
+ return users.size;
16
+ },
17
+ async findMany({ page, pageSize }) {
18
+ return [...users.values()].slice((page - 1) * pageSize, page * pageSize).map(publicUser);
19
+ },
20
+ async findById(id) {
21
+ const user = users.get(Number(id));
22
+ return user ? publicUser(user) : null;
23
+ },
24
+ async findByEmail(email) {
25
+ return [...users.values()].find((user) => user.email === email) ?? null;
26
+ },
27
+ async create(input) {
28
+ const now = new Date().toISOString();
29
+ const user: StoredUser = {
30
+ id: nextId++,
31
+ email: input.email,
32
+ passwordHash: input.passwordHash,
33
+ name: input.name ?? null,
34
+ role: input.role === 'ADMIN' ? 'ADMIN' : 'USER',
35
+ createdAt: now,
36
+ updatedAt: now,
37
+ profile: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
38
+ };
39
+ users.set(Number(user.id), user);
40
+ return publicUser(user);
41
+ },
42
+ async update(id, input) {
43
+ const user = users.get(Number(id));
44
+ if (!user) throw new Error('USER_NOT_FOUND');
45
+ const nextUser: StoredUser = {
46
+ ...user,
47
+ name: input.name ?? user.name,
48
+ role: input.role ?? user.role,
49
+ updatedAt: new Date().toISOString(),
50
+ profile: {
51
+ bio: input.bio ?? user.profile?.bio ?? null,
52
+ avatarUrl: input.avatarUrl ?? user.profile?.avatarUrl ?? null,
53
+ },
54
+ };
55
+ users.set(Number(id), nextUser);
56
+ return publicUser(nextUser);
57
+ },
58
+ async delete(id) {
59
+ users.delete(Number(id));
60
+ },
61
+ async updateMe(userId, input) {
62
+ return this.update(userId, input);
63
+ },
64
+ };
65
+
66
+ const app = createServer();
67
+ const lib = createLibrary({ routesPrefix: '/api' }, { userRepo });
68
+
69
+ app.use(lib.router);
70
+ app.listen(3000, () => console.log('API running on http://localhost:3000'));
@@ -0,0 +1,6 @@
1
+ DATABASE_URL="postgresql://app:app@localhost:5432/mycrud"
2
+ JWT_SECRET="replace-with-a-long-random-secret"
3
+ JWT_ACCESS_EXPIRES_IN="15m"
4
+ JWT_REFRESH_EXPIRES_IN="7d"
5
+ BCRYPT_SALT="10"
6
+ PORT="3000"
@@ -0,0 +1,25 @@
1
+ # Express + Prisma Example
2
+
3
+ Minimal app using `my-crud-lib` with Express and Prisma.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install
9
+ cp .env.example .env
10
+ npx prisma generate
11
+ npx prisma migrate dev --name init
12
+ npm run dev
13
+ ```
14
+
15
+ The API starts on `http://localhost:3000`.
16
+
17
+ ## Routes
18
+
19
+ - `POST /api/auth/register`
20
+ - `POST /api/auth/login`
21
+ - `POST /api/auth/refresh`
22
+ - `GET /api/auth/me`
23
+ - `GET /api/users/me`
24
+
25
+ Use `Authorization: Bearer <accessToken>` for protected routes.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "my-crud-lib-express-prisma-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/server.ts",
7
+ "prisma:generate": "prisma generate",
8
+ "prisma:migrate": "prisma migrate dev"
9
+ },
10
+ "dependencies": {
11
+ "@prisma/client": "^6.14.0",
12
+ "body-parser": "^1.20.2",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.3.1",
15
+ "express": "^4.18.2",
16
+ "my-crud-lib": "file:../.."
17
+ },
18
+ "devDependencies": {
19
+ "prisma": "^6.14.0",
20
+ "tsx": "^4.19.0",
21
+ "typescript": "^5.9.2"
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id Int @id @default(autoincrement())
12
+ email String @unique
13
+ passwordHash String
14
+ name String?
15
+ role Role @default(USER)
16
+ createdAt DateTime @default(now())
17
+ updatedAt DateTime @updatedAt
18
+ profile Profile?
19
+ }
20
+
21
+ model Profile {
22
+ id Int @id @default(autoincrement())
23
+ userId Int @unique
24
+ bio String?
25
+ avatarUrl String?
26
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
27
+ }
28
+
29
+ enum Role {
30
+ USER
31
+ ADMIN
32
+ }
@@ -0,0 +1,25 @@
1
+ import 'dotenv/config';
2
+ import { PrismaClient } from '@prisma/client';
3
+ import { createLibrary, createServer } from 'my-crud-lib';
4
+ import { makePrismaUserRepo } from 'my-crud-lib/adapter-prisma';
5
+
6
+ const prisma = new PrismaClient();
7
+ const app = createServer();
8
+
9
+ const lib = createLibrary(
10
+ {
11
+ routesPrefix: '/api',
12
+ auth: {
13
+ passwordHashRounds: Number(process.env.BCRYPT_SALT) || 10,
14
+ },
15
+ },
16
+ { userRepo: makePrismaUserRepo(prisma) },
17
+ );
18
+
19
+ app.use(lib.router);
20
+
21
+ const port = Number(process.env.PORT) || 3000;
22
+
23
+ app.listen(port, () => {
24
+ console.log(`API running on http://localhost:${port}`);
25
+ });
package/package.json CHANGED
@@ -1,24 +1,68 @@
1
1
  {
2
2
  "name": "my-crud-lib",
3
- "version": "1.0.3",
4
- "description": "Libreria CRUD modulare (Auth/User/Profile) con TS; Prisma opzionale via adapter",
3
+ "version": "2.0.0",
4
+ "description": "TypeScript auth and user/profile CRUD helpers for Express with Prisma and custom repository adapters",
5
5
  "license": "MIT",
6
+ "author": "Riccardo Sensi",
6
7
  "type": "module",
8
+ "sideEffects": false,
7
9
  "main": "dist/index.js",
8
10
  "types": "dist/index.d.ts",
11
+ "keywords": [
12
+ "auth",
13
+ "authentication",
14
+ "express",
15
+ "prisma",
16
+ "jwt",
17
+ "zod",
18
+ "typescript",
19
+ "crud",
20
+ "user-management",
21
+ "repository-pattern"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/riccardosensi99/CRUD-lib.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/riccardosensi99/CRUD-lib/issues"
29
+ },
30
+ "homepage": "https://github.com/riccardosensi99/CRUD-lib#readme",
9
31
  "exports": {
10
32
  ".": {
11
33
  "import": "./dist/index.js",
12
34
  "types": "./dist/index.d.ts"
13
35
  },
36
+ "./auth": {
37
+ "import": "./dist/auth.js",
38
+ "types": "./dist/auth.d.ts"
39
+ },
40
+ "./user": {
41
+ "import": "./dist/user.js",
42
+ "types": "./dist/user.d.ts"
43
+ },
44
+ "./schemas": {
45
+ "import": "./dist/schemas.js",
46
+ "types": "./dist/schemas.d.ts"
47
+ },
48
+ "./middleware": {
49
+ "import": "./dist/middleware.js",
50
+ "types": "./dist/middleware.d.ts"
51
+ },
14
52
  "./adapter-prisma": {
53
+ "import": "./dist/adapter-prisma.js",
54
+ "types": "./dist/adapter-prisma.d.ts"
55
+ },
56
+ "./adapters/prisma": {
15
57
  "import": "./dist/adapters/prisma.js",
16
58
  "types": "./dist/adapters/prisma.d.ts"
17
59
  }
18
60
  },
19
61
  "files": [
20
62
  "dist",
63
+ "examples",
21
64
  "README.md",
65
+ "RELEASE.md",
22
66
  "LICENSE",
23
67
  "prisma/schema.prisma"
24
68
  ],
@@ -29,7 +73,10 @@
29
73
  "schema": "prisma/schema.prisma"
30
74
  },
31
75
  "scripts": {
76
+ "prebuild": "node scripts/prisma-generate.mjs",
32
77
  "build": "tsc",
78
+ "test": "npm run build && node --test tests/*.test.mjs",
79
+ "ci": "npm test && npm run smoke:exports && npm run smoke:auth-hardening && npm run smoke:auth-service && npm --cache .npm-cache pack --dry-run",
33
80
  "start": "node dist/index.js",
34
81
  "dev": "node --loader ts-node/esm --no-warnings=ExperimentalWarning src/dev.ts",
35
82
  "dev:db:up": "docker-compose up -d",
@@ -38,6 +85,9 @@
38
85
  "prisma:migrate": "prisma migrate dev",
39
86
  "prisma:studio": "prisma studio",
40
87
  "prepublishOnly": "npm run build",
88
+ "smoke:exports": "npm run build && node scripts/smoke-public-api.mjs",
89
+ "smoke:auth-hardening": "npm run build && node scripts/smoke-auth-hardening.mjs",
90
+ "smoke:auth-service": "npm run build && node scripts/smoke-auth-service.mjs",
41
91
  "db:schema": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/schema.sql",
42
92
  "db:seed": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/seed.sql",
43
93
  "db:init": "npm run db:schema && npm run db:seed",