my-crud-lib 1.0.4 → 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.
- package/LICENSE +21 -0
- package/README.md +186 -131
- package/RELEASE.md +32 -0
- package/dist/adapter-prisma.d.ts +1 -0
- package/dist/adapter-prisma.js +1 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +4 -0
- package/dist/config/env.d.ts +3 -2
- package/dist/config/env.js +9 -3
- package/dist/dev.js +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +27 -15
- package/dist/middleware/hasRole.js +1 -2
- package/dist/middleware/isAuth.d.ts +1 -1
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +2 -0
- package/dist/modules/auth/auth.controller.d.ts +2 -1
- package/dist/modules/auth/auth.controller.js +11 -22
- package/dist/modules/auth/auth.defaults.d.ts +3 -0
- package/dist/modules/auth/auth.defaults.js +4 -0
- package/dist/modules/auth/auth.service.d.ts +8 -24
- package/dist/modules/auth/auth.service.js +54 -32
- package/dist/modules/auth/auth.types.d.ts +16 -1
- package/dist/modules/user/user.schemas.d.ts +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +2 -0
- package/dist/user.d.ts +5 -0
- package/dist/user.js +3 -0
- package/dist/utils/jwt.d.ts +1 -1
- package/dist/utils/jwt.js +4 -4
- package/examples/custom-repo/README.md +5 -0
- package/examples/custom-repo/server.ts +70 -0
- package/examples/express-prisma/.env.example +6 -0
- package/examples/express-prisma/README.md +25 -0
- package/examples/express-prisma/package.json +23 -0
- package/examples/express-prisma/prisma/schema.prisma +32 -0
- package/examples/express-prisma/src/server.ts +25 -0
- package/package.json +52 -14
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
|
-
|
|
3
|
+
[](https://github.com/riccardosensi99/CRUD-lib/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/my-crud-lib)
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
If you use the bundled Prisma adapter:
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
npm i
|
|
24
|
-
|
|
25
|
-
npm i @prisma/client
|
|
25
|
+
npm i @prisma/client prisma
|
|
26
|
+
npx prisma generate
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Node.js `>=18.17` is required.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
## Environment
|
|
31
32
|
|
|
32
|
-
|
|
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 {
|
|
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, () =>
|
|
65
|
+
app.listen(3000, () => {
|
|
66
|
+
console.log("API running on http://localhost:3000");
|
|
67
|
+
});
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
86
|
+
## Examples
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
91
|
+
Run the Prisma example:
|
|
90
92
|
|
|
91
|
-
```
|
|
92
|
-
|
|
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
|
-
|
|
104
|
+
Register:
|
|
100
105
|
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
import { makeCreateUserSchema } from "my-crud-lib/schemas";
|
|
106
|
+
```http
|
|
107
|
+
POST /api/auth/register
|
|
108
|
+
Content-Type: application/json
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
{
|
|
111
|
+
"email": "reader@example.com",
|
|
112
|
+
"password": "password123",
|
|
113
|
+
"name": "Reader"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
Response:
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
---
|
|
132
|
+
Login:
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
```http
|
|
135
|
+
POST /api/auth/login
|
|
136
|
+
Content-Type: application/json
|
|
122
137
|
|
|
123
|
-
|
|
138
|
+
{
|
|
139
|
+
"email": "reader@example.com",
|
|
140
|
+
"password": "password123"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
import { plugins } from "my-crud-lib";
|
|
144
|
+
Protected request:
|
|
127
145
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
143
|
-
- `beforeCreateUser(data, ctx)`
|
|
144
|
-
- `afterCreateUser(user, ctx)`
|
|
145
|
-
- `beforeUpdateUser(data, ctx)`
|
|
146
|
-
- `beforeIssueJwt(payload, ctx)`
|
|
151
|
+
## Public Imports
|
|
147
152
|
|
|
148
|
-
|
|
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
|
|
172
|
+
## Repository Adapter
|
|
153
173
|
|
|
154
|
-
|
|
174
|
+
User CRUD is driven by the `UserRepo` interface:
|
|
155
175
|
|
|
156
176
|
```ts
|
|
157
177
|
export interface UserRepo {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
206
|
+
The Prisma adapter is available from both import paths:
|
|
166
207
|
|
|
167
208
|
```ts
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
240
|
+
## Troubleshooting
|
|
191
241
|
|
|
192
|
-
|
|
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
|
-
|
|
246
|
+
`JWT_SECRET is required before signing or verifying tokens`
|
|
200
247
|
|
|
201
|
-
|
|
248
|
+
Set `JWT_SECRET` before mounting or calling auth routes. Use a long random value.
|
|
202
249
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
254
|
+
ESM import errors
|
|
207
255
|
|
|
208
|
-
|
|
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
|
|
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';
|
package/dist/config/env.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export declare const JWT_ACCESS_EXPIRES_IN: string
|
|
2
|
-
export declare const
|
|
1
|
+
export declare const JWT_ACCESS_EXPIRES_IN: string;
|
|
2
|
+
export declare const JWT_REFRESH_EXPIRES_IN: string;
|
|
3
|
+
export declare function getJwtSecret(): string;
|
package/dist/config/env.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import dotenv from "dotenv";
|
|
2
|
-
import { env } from "process";
|
|
3
2
|
dotenv.config();
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
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,7 +1,27 @@
|
|
|
1
1
|
import { type Express } from 'express';
|
|
2
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';
|
|
4
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';
|
|
5
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
|
+
};
|
|
6
23
|
export declare function createServer(): Express;
|
|
7
|
-
export declare function
|
|
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,28 +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
4
|
export { createUserRouter } from './modules/user/user.controller.js';
|
|
5
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';
|
|
6
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
|
+
}
|
|
7
24
|
export function createServer() {
|
|
8
25
|
const app = express();
|
|
9
26
|
app.use(cors());
|
|
10
27
|
app.use(bodyParser.json());
|
|
11
28
|
return app;
|
|
12
29
|
}
|
|
13
|
-
export function
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
function requireUserRouter(deps) {
|
|
20
|
-
// @ts-ignore: compiled JS avrà il file corretto
|
|
21
|
-
const { createUserRouter } = require('./modules/user/user.router.js');
|
|
22
|
-
return createUserRouter({ userRepo: deps.userRepo });
|
|
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 };
|
|
23
36
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return createAuthRouter( /* es: { userRepo: deps.userRepo, jwt: {...} } */);
|
|
37
|
+
export function mountDefaultRoutes(app, deps, config = {}) {
|
|
38
|
+
const { router } = createLibrary(config, deps);
|
|
39
|
+
app.use(router);
|
|
28
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
|
-
|
|
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 +1,2 @@
|
|
|
1
|
-
|
|
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 {
|
|
7
|
-
|
|
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
|
|
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: '
|
|
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
|
|
43
|
-
|
|
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
|
|
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);
|
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
import type { RegisterInput } from './auth.schemas.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
@@ -14,9 +14,9 @@ export declare const listUsersQuerySchema: z.ZodObject<{
|
|
|
14
14
|
search?: string | undefined;
|
|
15
15
|
}, {
|
|
16
16
|
role?: "USER" | "ADMIN" | undefined;
|
|
17
|
+
search?: string | undefined;
|
|
17
18
|
page?: number | undefined;
|
|
18
19
|
pageSize?: number | undefined;
|
|
19
|
-
search?: string | undefined;
|
|
20
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<{
|
package/dist/schemas.js
ADDED
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';
|
package/dist/utils/jwt.d.ts
CHANGED
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
|
|
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,
|
|
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" },
|
|
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,
|
|
14
|
+
return jwt.verify(token, getJwtSecret());
|
|
15
15
|
}
|
|
@@ -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,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,36 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-crud-lib",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
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
|
+
},
|
|
52
|
+
"./adapter-prisma": {
|
|
53
|
+
"import": "./dist/adapter-prisma.js",
|
|
54
|
+
"types": "./dist/adapter-prisma.d.ts"
|
|
55
|
+
},
|
|
14
56
|
"./adapters/prisma": {
|
|
15
57
|
"import": "./dist/adapters/prisma.js",
|
|
16
58
|
"types": "./dist/adapters/prisma.d.ts"
|
|
17
|
-
},
|
|
18
|
-
"./modules/user/user.router": {
|
|
19
|
-
"import": "./dist/modules/user/user.router.js",
|
|
20
|
-
"types": "./dist/modules/user/user.router.d.ts"
|
|
21
|
-
},
|
|
22
|
-
"./modules/auth/auth.router": {
|
|
23
|
-
"import": "./dist/modules/auth/auth.router.js",
|
|
24
|
-
"types": "./dist/modules/auth/auth.router.d.ts"
|
|
25
59
|
}
|
|
26
60
|
},
|
|
27
|
-
"./adapter-prisma": {
|
|
28
|
-
"import": "./dist/adapters/prisma.js",
|
|
29
|
-
"types": "./dist/adapters/prisma.d.ts"
|
|
30
|
-
},
|
|
31
61
|
"files": [
|
|
32
62
|
"dist",
|
|
63
|
+
"examples",
|
|
33
64
|
"README.md",
|
|
65
|
+
"RELEASE.md",
|
|
34
66
|
"LICENSE",
|
|
35
67
|
"prisma/schema.prisma"
|
|
36
68
|
],
|
|
@@ -41,7 +73,10 @@
|
|
|
41
73
|
"schema": "prisma/schema.prisma"
|
|
42
74
|
},
|
|
43
75
|
"scripts": {
|
|
76
|
+
"prebuild": "node scripts/prisma-generate.mjs",
|
|
44
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",
|
|
45
80
|
"start": "node dist/index.js",
|
|
46
81
|
"dev": "node --loader ts-node/esm --no-warnings=ExperimentalWarning src/dev.ts",
|
|
47
82
|
"dev:db:up": "docker-compose up -d",
|
|
@@ -50,6 +85,9 @@
|
|
|
50
85
|
"prisma:migrate": "prisma migrate dev",
|
|
51
86
|
"prisma:studio": "prisma studio",
|
|
52
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",
|
|
53
91
|
"db:schema": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/schema.sql",
|
|
54
92
|
"db:seed": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/seed.sql",
|
|
55
93
|
"db:init": "npm run db:schema && npm run db:seed",
|