my-crud-lib 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/dist/config/env.d.ts +2 -0
- package/dist/config/env.js +5 -0
- package/dist/database/prisma.service.d.ts +1 -0
- package/dist/database/prisma.service.js +1 -0
- package/dist/dev.d.ts +1 -0
- package/dist/dev.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -0
- package/dist/middleware/hasRole.d.ts +4 -0
- package/dist/middleware/hasRole.js +19 -0
- package/dist/middleware/isAuth.d.ts +8 -0
- package/dist/middleware/isAuth.js +16 -0
- package/dist/modules/auth/auth.controller.d.ts +1 -0
- package/dist/modules/auth/auth.controller.js +67 -0
- package/dist/modules/auth/auth.schemas.d.ts +26 -0
- package/dist/modules/auth/auth.schemas.js +10 -0
- package/dist/modules/auth/auth.service.d.ts +24 -0
- package/dist/modules/auth/auth.service.js +38 -0
- package/dist/modules/auth/auth.types.d.ts +1 -0
- package/dist/modules/auth/auth.types.js +1 -0
- package/dist/modules/profile/profile.controller.d.ts +1 -0
- package/dist/modules/profile/profile.controller.js +1 -0
- package/dist/modules/profile/profile.service.d.ts +1 -0
- package/dist/modules/profile/profile.service.js +1 -0
- package/dist/modules/profile/profile.types.d.ts +1 -0
- package/dist/modules/profile/profile.types.js +1 -0
- package/dist/modules/user/user.controller.d.ts +1 -0
- package/dist/modules/user/user.controller.js +86 -0
- package/dist/modules/user/user.schemas.d.ts +75 -0
- package/dist/modules/user/user.schemas.js +30 -0
- package/dist/modules/user/user.service.d.ts +68 -0
- package/dist/modules/user/user.service.js +136 -0
- package/dist/modules/user/user.types.d.ts +1 -0
- package/dist/modules/user/user.types.js +1 -0
- package/dist/utils/bcrypt.d.ts +1 -0
- package/dist/utils/bcrypt.js +1 -0
- package/dist/utils/errorHandler.d.ts +1 -0
- package/dist/utils/errorHandler.js +1 -0
- package/dist/utils/jwt.d.ts +8 -0
- package/dist/utils/jwt.js +15 -0
- package/dist/utils/prisma.d.ts +5 -0
- package/dist/utils/prisma.js +4 -0
- package/package.json +66 -0
- package/prisma/schema.prisma +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# my-crud-lib
|
|
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.
|
|
4
|
+
|
|
5
|
+
> Works great with Express and Prisma out of the box, but you can plug in your own repo adapter.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
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
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm i my-crud-lib zod jsonwebtoken bcryptjs
|
|
24
|
+
# If using Prisma adapter in your app:
|
|
25
|
+
npm i @prisma/client
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> Node.js `>= 18.17` is required.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart (Express)
|
|
33
|
+
|
|
34
|
+
```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
|
+
import { PrismaClient } from "@prisma/client";
|
|
40
|
+
import { makePrismaUserRepo } from "my-crud-lib/adapter-prisma"; // if you expose this path
|
|
41
|
+
|
|
42
|
+
const prisma = new PrismaClient();
|
|
43
|
+
|
|
44
|
+
const app = express();
|
|
45
|
+
app.use(json());
|
|
46
|
+
|
|
47
|
+
const lib = createLibrary(
|
|
48
|
+
{
|
|
49
|
+
auth: {
|
|
50
|
+
jwtSecret: process.env.JWT_SECRET!, // e.g. "supersecret"
|
|
51
|
+
jwtExpiresIn: "7d",
|
|
52
|
+
passwordHashRounds: 10,
|
|
53
|
+
},
|
|
54
|
+
routesPrefix: "/api", // optional
|
|
55
|
+
},
|
|
56
|
+
{ userRepo: makePrismaUserRepo(prisma) }
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
app.use(lib.router);
|
|
60
|
+
|
|
61
|
+
app.listen(3000, () => console.log("API running on http://localhost:3000"));
|
|
62
|
+
```
|
|
63
|
+
|
|
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
|
|
69
|
+
|
|
70
|
+
> Protect `/me` with the `isAuth` middleware already wired inside the library router.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
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
|
+
|
|
89
|
+
Create the library:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const lib = createLibrary(config, { userRepo });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Extending Schemas (Zod)
|
|
98
|
+
|
|
99
|
+
The library exports base Zod schemas and a factory to merge your custom fields.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// consumer app
|
|
103
|
+
import { z } from "zod";
|
|
104
|
+
import { makeCreateUserSchema } from "my-crud-lib/schemas";
|
|
105
|
+
|
|
106
|
+
const ExtraUserFields = z.object({
|
|
107
|
+
companyVat: z.string().min(5),
|
|
108
|
+
marketingOptIn: z.boolean().default(false),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const CreateUserSchema = makeCreateUserSchema(ExtraUserFields);
|
|
112
|
+
|
|
113
|
+
// Later in your route (if you override the built-in):
|
|
114
|
+
const data = CreateUserSchema.parse(req.body);
|
|
115
|
+
```
|
|
116
|
+
|
|
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
|
+
---
|
|
120
|
+
|
|
121
|
+
## Hooks (Lifecycle)
|
|
122
|
+
|
|
123
|
+
Use hooks to change data or enrich tokens without forking.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { plugins } from "my-crud-lib";
|
|
127
|
+
|
|
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
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Available hooks**
|
|
143
|
+
- `beforeCreateUser(data, ctx)`
|
|
144
|
+
- `afterCreateUser(user, ctx)`
|
|
145
|
+
- `beforeUpdateUser(data, ctx)`
|
|
146
|
+
- `beforeIssueJwt(payload, ctx)`
|
|
147
|
+
|
|
148
|
+
`ctx` includes the request and useful dependencies (e.g., repos).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Repository Adapters (DB-agnostic)
|
|
153
|
+
|
|
154
|
+
Core interface:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
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>;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Example Prisma adapter (in your app or provided by the lib):
|
|
166
|
+
|
|
167
|
+
```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
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Types
|
|
181
|
+
|
|
182
|
+
The package exports the main public types:
|
|
183
|
+
|
|
184
|
+
- `LibraryConfig`, `AuthConfig`
|
|
185
|
+
- `UserRepo`
|
|
186
|
+
- schema types (e.g., `CreateUserBase`)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Security Notes
|
|
191
|
+
|
|
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.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
PRs and issues are welcome! Please follow conventional commits or include a clear description. For releases, we recommend Changesets or semantic-release.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT © Riccardo
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { env } from "process";
|
|
3
|
+
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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'dotenv/config';
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createServer } from './index.js';
|
|
2
|
+
import { createAuthRouter } from './modules/auth/auth.controller.js';
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
import { createUserRouter } from './modules/user/user.controller.js';
|
|
5
|
+
const app = createServer();
|
|
6
|
+
app.use('/auth', createAuthRouter());
|
|
7
|
+
app.use('/users', createUserRouter());
|
|
8
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
9
|
+
app.listen(PORT, () => {
|
|
10
|
+
console.log(`API dev up on http://localhost:${PORT}`);
|
|
11
|
+
console.log(` -> POST /auth/register, /auth/login, POST /auth/refresh, GET /auth/me`);
|
|
12
|
+
console.log(` -> GET /users/me, PUT /users/me`);
|
|
13
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import bodyParser from 'body-parser';
|
|
4
|
+
export { createAuthRouter } from './modules/auth/auth.controller.js';
|
|
5
|
+
//export { createUserRouter } from './modules/user/user.router.js';
|
|
6
|
+
export function createServer() {
|
|
7
|
+
const app = express();
|
|
8
|
+
app.use(cors());
|
|
9
|
+
app.use(bodyParser.json());
|
|
10
|
+
return app;
|
|
11
|
+
}
|
|
12
|
+
export function mountDefaultRoutes(app) {
|
|
13
|
+
const { createAuthRouter } = require('./modules/auth/auth.controller.js');
|
|
14
|
+
const { createUserRouter } = require('./modules/user/user.router.js');
|
|
15
|
+
app.use('/auth', createAuthRouter());
|
|
16
|
+
app.use('/users', createUserRouter());
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Response, NextFunction } from 'express';
|
|
2
|
+
import { AuthRequest } from './isAuth.js';
|
|
3
|
+
export declare function hasRole(...allowed: Array<'ADMIN' | 'USER'>): (req: AuthRequest, res: Response, next: NextFunction) => Response<any, Record<string, any>> | undefined;
|
|
4
|
+
export declare function isSelfOrAdmin(): (req: AuthRequest, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function hasRole(...allowed) {
|
|
2
|
+
return (req, res, next) => {
|
|
3
|
+
const role = req.user?.role;
|
|
4
|
+
if (!role || !allowed.includes(role)) {
|
|
5
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
6
|
+
}
|
|
7
|
+
next();
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function isSelfOrAdmin() {
|
|
11
|
+
return (req, res, next) => {
|
|
12
|
+
const uid = req.user?.id;
|
|
13
|
+
const role = req.user?.role;
|
|
14
|
+
const paramId = Number(req.params.id);
|
|
15
|
+
if (role === 'ADMIN' || uid === paramId)
|
|
16
|
+
return next();
|
|
17
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export type AuthRequest = Request & {
|
|
3
|
+
user?: {
|
|
4
|
+
id: number;
|
|
5
|
+
role: 'USER' | 'ADMIN';
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export declare function isAuth(req: AuthRequest, res: Response, next: NextFunction): void | Response<any, Record<string, any>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { verifyToken } from '../utils/jwt.js';
|
|
2
|
+
export function isAuth(req, res, next) {
|
|
3
|
+
const auth = req.headers.authorization;
|
|
4
|
+
if (!auth?.startsWith('Bearer ')) {
|
|
5
|
+
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
6
|
+
}
|
|
7
|
+
const token = auth.substring('Bearer '.length);
|
|
8
|
+
try {
|
|
9
|
+
const payload = verifyToken(token);
|
|
10
|
+
req.user = { id: payload.sub, role: payload.role };
|
|
11
|
+
return next();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createAuthRouter(): import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
import { isAuth } from '../../middleware/isAuth.js';
|
|
6
|
+
import { prisma } from '../../utils/prisma.js';
|
|
7
|
+
export function createAuthRouter() {
|
|
8
|
+
const router = Router();
|
|
9
|
+
router.post('/register', async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const data = registerSchema.parse(req.body);
|
|
12
|
+
const result = await registerUser(data);
|
|
13
|
+
return res.status(201).json(result);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (err?.message === 'EMAIL_TAKEN')
|
|
17
|
+
return res.status(409).json({ error: 'Email già registrata' });
|
|
18
|
+
if (err?.issues)
|
|
19
|
+
return res.status(400).json({ error: 'ValidationError', details: err.issues });
|
|
20
|
+
return res.status(500).json({ error: 'InternalError' });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
router.post('/login', async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const data = loginSchema.parse(req.body);
|
|
26
|
+
const result = await loginUser(data);
|
|
27
|
+
return res.json(result);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err?.message === 'INVALID_CREDENTIALS')
|
|
31
|
+
return res.status(401).json({ error: 'Credenziali non valide' });
|
|
32
|
+
if (err?.issues)
|
|
33
|
+
return res.status(400).json({ error: 'ValidationError', details: err.issues });
|
|
34
|
+
return res.status(500).json({ error: 'InternalError' });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
router.post('/refresh', async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { refreshToken } = req.body;
|
|
40
|
+
if (!refreshToken)
|
|
41
|
+
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 });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return res.status(401).json({ error: 'Invalid or expired refresh token' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
router.get('/me', isAuth, async (req, res) => {
|
|
57
|
+
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
|
+
});
|
|
62
|
+
if (!me)
|
|
63
|
+
return res.status(404).json({ error: 'User not found' });
|
|
64
|
+
return res.json(me);
|
|
65
|
+
});
|
|
66
|
+
return router;
|
|
67
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const registerSchema: z.ZodObject<{
|
|
3
|
+
email: z.ZodString;
|
|
4
|
+
password: z.ZodString;
|
|
5
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
email: string;
|
|
8
|
+
password: string;
|
|
9
|
+
name?: string | null | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
email: string;
|
|
12
|
+
password: string;
|
|
13
|
+
name?: string | null | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const loginSchema: z.ZodObject<{
|
|
16
|
+
email: z.ZodString;
|
|
17
|
+
password: z.ZodString;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
email: string;
|
|
20
|
+
password: string;
|
|
21
|
+
}, {
|
|
22
|
+
email: string;
|
|
23
|
+
password: string;
|
|
24
|
+
}>;
|
|
25
|
+
export type RegisterInput = z.infer<typeof registerSchema>;
|
|
26
|
+
export type LoginInput = z.infer<typeof loginSchema>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const registerSchema = z.object({
|
|
3
|
+
email: z.string().email(),
|
|
4
|
+
password: z.string().min(8),
|
|
5
|
+
name: z.string().min(1).max(100).nullish(),
|
|
6
|
+
});
|
|
7
|
+
export const loginSchema = z.object({
|
|
8
|
+
email: z.string().email(),
|
|
9
|
+
password: z.string().min(8),
|
|
10
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { prisma } from '../../utils/prisma.js';
|
|
2
|
+
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 };
|
|
22
|
+
}
|
|
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');
|
|
30
|
+
const payload = { sub: user.id, role: user.role };
|
|
31
|
+
const accessToken = signAccessToken(payload);
|
|
32
|
+
const refreshToken = signRefreshToken(payload);
|
|
33
|
+
return {
|
|
34
|
+
user: { id: user.id, email: user.email, name: user.name, role: user.role },
|
|
35
|
+
accessToken,
|
|
36
|
+
refreshToken,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createUserRouter(): import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { isAuth } from '../../middleware/isAuth.js';
|
|
3
|
+
import { hasRole, isSelfOrAdmin } from '../../middleware/hasRole.js';
|
|
4
|
+
import { listUsersQuerySchema, updateMeSchema, adminCreateUserSchema, adminUpdateUserSchema, } from './user.schemas.js';
|
|
5
|
+
import { listUsers, getUserById, updateMe, adminCreateUser, adminUpdateUser, adminDeleteUser, } from './user.service.js';
|
|
6
|
+
export function createUserRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
router.get('/', isAuth, hasRole('ADMIN'), async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const q = listUsersQuerySchema.parse(req.query);
|
|
11
|
+
const data = await listUsers(q);
|
|
12
|
+
res.json(data);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
if (e?.issues)
|
|
16
|
+
return res.status(400).json({ error: 'ValidationError', details: e.issues });
|
|
17
|
+
res.status(500).json({ error: 'InternalError' });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
router.get('/:id', isAuth, isSelfOrAdmin(), async (req, res) => {
|
|
21
|
+
const id = Number(req.params.id);
|
|
22
|
+
const data = await getUserById(id);
|
|
23
|
+
if (!data)
|
|
24
|
+
return res.status(404).json({ error: 'User not found' });
|
|
25
|
+
res.json(data);
|
|
26
|
+
});
|
|
27
|
+
router.get('/me/self', isAuth, async (req, res) => {
|
|
28
|
+
const data = await getUserById(req.user.id);
|
|
29
|
+
if (!data)
|
|
30
|
+
return res.status(404).json({ error: 'User not found' });
|
|
31
|
+
res.json(data);
|
|
32
|
+
});
|
|
33
|
+
router.put('/me', isAuth, async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const body = updateMeSchema.parse(req.body);
|
|
36
|
+
const data = await updateMe(req.user.id, body);
|
|
37
|
+
res.json(data);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (e?.issues)
|
|
41
|
+
return res.status(400).json({ error: 'ValidationError', details: e.issues });
|
|
42
|
+
res.status(500).json({ error: 'InternalError' });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
router.post('/', isAuth, hasRole('ADMIN'), async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const body = adminCreateUserSchema.parse(req.body);
|
|
48
|
+
const data = await adminCreateUser(body);
|
|
49
|
+
res.status(201).json(data);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e?.message === 'EMAIL_TAKEN')
|
|
53
|
+
return res.status(409).json({ error: 'Email already in use' });
|
|
54
|
+
if (e?.issues)
|
|
55
|
+
return res.status(400).json({ error: 'ValidationError', details: e.issues });
|
|
56
|
+
res.status(500).json({ error: 'InternalError' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
router.put('/:id', isAuth, isSelfOrAdmin(), async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const id = Number(req.params.id);
|
|
62
|
+
const body = adminUpdateUserSchema.parse(req.body);
|
|
63
|
+
if (req.user.role !== 'ADMIN' && body.role) {
|
|
64
|
+
return res.status(403).json({ error: 'Forbidden: cannot change role' });
|
|
65
|
+
}
|
|
66
|
+
const data = await adminUpdateUser(id, body);
|
|
67
|
+
res.json(data);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (e?.issues)
|
|
71
|
+
return res.status(400).json({ error: 'ValidationError', details: e.issues });
|
|
72
|
+
res.status(500).json({ error: 'InternalError' });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
router.delete('/:id', isAuth, hasRole('ADMIN'), async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const id = Number(req.params.id);
|
|
78
|
+
const data = await adminDeleteUser(id);
|
|
79
|
+
res.json(data);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
res.status(500).json({ error: 'InternalError' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return router;
|
|
86
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const listUsersQuerySchema: z.ZodObject<{
|
|
3
|
+
page: z.ZodDefault<z.ZodNumber>;
|
|
4
|
+
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
5
|
+
search: z.ZodOptional<z.ZodString>;
|
|
6
|
+
role: z.ZodOptional<z.ZodEnum<["USER", "ADMIN"]>>;
|
|
7
|
+
sort: z.ZodDefault<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
sort: string;
|
|
10
|
+
page: number;
|
|
11
|
+
pageSize: number;
|
|
12
|
+
search?: string | undefined;
|
|
13
|
+
role?: "USER" | "ADMIN" | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
sort?: string | undefined;
|
|
16
|
+
search?: string | undefined;
|
|
17
|
+
role?: "USER" | "ADMIN" | undefined;
|
|
18
|
+
page?: number | undefined;
|
|
19
|
+
pageSize?: number | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const updateMeSchema: z.ZodObject<{
|
|
22
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
23
|
+
bio: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
+
avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
name?: string | null | undefined;
|
|
27
|
+
bio?: string | null | undefined;
|
|
28
|
+
avatarUrl?: string | null | undefined;
|
|
29
|
+
}, {
|
|
30
|
+
name?: string | null | undefined;
|
|
31
|
+
bio?: string | null | undefined;
|
|
32
|
+
avatarUrl?: string | null | undefined;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const adminCreateUserSchema: z.ZodObject<{
|
|
35
|
+
email: z.ZodString;
|
|
36
|
+
password: z.ZodString;
|
|
37
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
38
|
+
role: z.ZodDefault<z.ZodEnum<["USER", "ADMIN"]>>;
|
|
39
|
+
bio: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
40
|
+
avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
email: string;
|
|
43
|
+
password: string;
|
|
44
|
+
role: "USER" | "ADMIN";
|
|
45
|
+
name?: string | null | undefined;
|
|
46
|
+
bio?: string | null | undefined;
|
|
47
|
+
avatarUrl?: string | null | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
email: string;
|
|
50
|
+
password: string;
|
|
51
|
+
name?: string | null | undefined;
|
|
52
|
+
role?: "USER" | "ADMIN" | undefined;
|
|
53
|
+
bio?: string | null | undefined;
|
|
54
|
+
avatarUrl?: string | null | undefined;
|
|
55
|
+
}>;
|
|
56
|
+
export declare const adminUpdateUserSchema: z.ZodObject<{
|
|
57
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodEnum<["USER", "ADMIN"]>>>;
|
|
59
|
+
bio: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
60
|
+
avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
name?: string | null | undefined;
|
|
63
|
+
role?: "USER" | "ADMIN" | null | undefined;
|
|
64
|
+
bio?: string | null | undefined;
|
|
65
|
+
avatarUrl?: string | null | undefined;
|
|
66
|
+
}, {
|
|
67
|
+
name?: string | null | undefined;
|
|
68
|
+
role?: "USER" | "ADMIN" | null | undefined;
|
|
69
|
+
bio?: string | null | undefined;
|
|
70
|
+
avatarUrl?: string | null | undefined;
|
|
71
|
+
}>;
|
|
72
|
+
export type ListUsersQuery = z.infer<typeof listUsersQuerySchema>;
|
|
73
|
+
export type UpdateMeInput = z.infer<typeof updateMeSchema>;
|
|
74
|
+
export type AdminCreateUserInput = z.infer<typeof adminCreateUserSchema>;
|
|
75
|
+
export type AdminUpdateUserInput = z.infer<typeof adminUpdateUserSchema>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const listUsersQuerySchema = z.object({
|
|
3
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
4
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(10),
|
|
5
|
+
search: z.string().trim().min(1).optional(),
|
|
6
|
+
role: z.enum(['USER', 'ADMIN']).optional(),
|
|
7
|
+
sort: z
|
|
8
|
+
.string()
|
|
9
|
+
.regex(/^(createdAt|updatedAt|email|name):(asc|desc)$/i)
|
|
10
|
+
.default('createdAt:desc'),
|
|
11
|
+
});
|
|
12
|
+
export const updateMeSchema = z.object({
|
|
13
|
+
name: z.string().min(1).max(100).nullish(),
|
|
14
|
+
bio: z.string().max(500).nullish(),
|
|
15
|
+
avatarUrl: z.string().url().nullish(),
|
|
16
|
+
});
|
|
17
|
+
export const adminCreateUserSchema = z.object({
|
|
18
|
+
email: z.string().email(),
|
|
19
|
+
password: z.string().min(8),
|
|
20
|
+
name: z.string().min(1).max(100).nullish(),
|
|
21
|
+
role: z.enum(['USER', 'ADMIN']).default('USER'),
|
|
22
|
+
bio: z.string().max(500).nullish(),
|
|
23
|
+
avatarUrl: z.string().url().nullish(),
|
|
24
|
+
});
|
|
25
|
+
export const adminUpdateUserSchema = z.object({
|
|
26
|
+
name: z.string().min(1).max(100).nullish(),
|
|
27
|
+
role: z.enum(['USER', 'ADMIN']).nullish(),
|
|
28
|
+
bio: z.string().max(500).nullish(),
|
|
29
|
+
avatarUrl: z.string().url().nullish(),
|
|
30
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AdminCreateUserInput, AdminUpdateUserInput, ListUsersQuery, UpdateMeInput } from './user.schemas.js';
|
|
2
|
+
export declare function listUsers(q: ListUsersQuery): Promise<{
|
|
3
|
+
page: number;
|
|
4
|
+
pageSize: number;
|
|
5
|
+
total: number;
|
|
6
|
+
items: {
|
|
7
|
+
email: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
id: number;
|
|
10
|
+
role: import("@prisma/client").$Enums.Role;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
profile: {
|
|
14
|
+
bio: string | null;
|
|
15
|
+
avatarUrl: string | null;
|
|
16
|
+
} | null;
|
|
17
|
+
}[];
|
|
18
|
+
totalPages: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function getUserById(id: number): Promise<{
|
|
21
|
+
email: string;
|
|
22
|
+
name: string | null;
|
|
23
|
+
id: number;
|
|
24
|
+
role: import("@prisma/client").$Enums.Role;
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
updatedAt: Date;
|
|
27
|
+
profile: {
|
|
28
|
+
bio: string | null;
|
|
29
|
+
avatarUrl: string | null;
|
|
30
|
+
} | null;
|
|
31
|
+
} | null>;
|
|
32
|
+
export declare function updateMe(userId: number, data: UpdateMeInput): Promise<{
|
|
33
|
+
email: string;
|
|
34
|
+
name: string | null;
|
|
35
|
+
id: number;
|
|
36
|
+
role: import("@prisma/client").$Enums.Role;
|
|
37
|
+
profile: {
|
|
38
|
+
bio: string | null;
|
|
39
|
+
avatarUrl: string | null;
|
|
40
|
+
} | null;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function adminCreateUser(input: AdminCreateUserInput): Promise<{
|
|
43
|
+
email: string;
|
|
44
|
+
name: string | null;
|
|
45
|
+
id: number;
|
|
46
|
+
role: import("@prisma/client").$Enums.Role;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
updatedAt: Date;
|
|
49
|
+
profile: {
|
|
50
|
+
bio: string | null;
|
|
51
|
+
avatarUrl: string | null;
|
|
52
|
+
} | null;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function adminUpdateUser(id: number, input: AdminUpdateUserInput): Promise<{
|
|
55
|
+
email: string;
|
|
56
|
+
name: string | null;
|
|
57
|
+
id: number;
|
|
58
|
+
role: import("@prisma/client").$Enums.Role;
|
|
59
|
+
createdAt: Date;
|
|
60
|
+
updatedAt: Date;
|
|
61
|
+
profile: {
|
|
62
|
+
bio: string | null;
|
|
63
|
+
avatarUrl: string | null;
|
|
64
|
+
} | null;
|
|
65
|
+
}>;
|
|
66
|
+
export declare function adminDeleteUser(id: number): Promise<{
|
|
67
|
+
ok: boolean;
|
|
68
|
+
}>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { prisma } from '../../utils/prisma.js';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
export async function listUsers(q) {
|
|
4
|
+
const { page, pageSize, role, search } = q;
|
|
5
|
+
const [rawField, rawDir] = (q.sort ?? 'createdAt:desc').split(':');
|
|
6
|
+
const allowedFields = ['createdAt', 'updatedAt', 'email', 'name'];
|
|
7
|
+
const sortField = allowedFields.includes(rawField || '')
|
|
8
|
+
? rawField
|
|
9
|
+
: 'createdAt';
|
|
10
|
+
const sortDir = rawDir === 'asc' || rawDir === 'desc' ? rawDir : 'desc';
|
|
11
|
+
const where = {};
|
|
12
|
+
if (role)
|
|
13
|
+
where.role = role;
|
|
14
|
+
if (search && search.trim() !== '') {
|
|
15
|
+
const s = search.trim();
|
|
16
|
+
where.OR = [
|
|
17
|
+
{ email: { contains: s, mode: 'insensitive' } },
|
|
18
|
+
{ name: { contains: s, mode: 'insensitive' } },
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
const orderBy = { [sortField]: sortDir };
|
|
22
|
+
const [total, items] = await Promise.all([
|
|
23
|
+
prisma.user.count({ where }),
|
|
24
|
+
prisma.user.findMany({
|
|
25
|
+
where,
|
|
26
|
+
orderBy,
|
|
27
|
+
skip: (page - 1) * pageSize,
|
|
28
|
+
take: pageSize,
|
|
29
|
+
select: {
|
|
30
|
+
id: true,
|
|
31
|
+
email: true,
|
|
32
|
+
name: true,
|
|
33
|
+
role: true,
|
|
34
|
+
createdAt: true,
|
|
35
|
+
updatedAt: true,
|
|
36
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
]);
|
|
40
|
+
return {
|
|
41
|
+
page,
|
|
42
|
+
pageSize,
|
|
43
|
+
total,
|
|
44
|
+
items,
|
|
45
|
+
totalPages: Math.ceil(total / pageSize),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function getUserById(id) {
|
|
49
|
+
return prisma.user.findUnique({
|
|
50
|
+
where: { id },
|
|
51
|
+
select: {
|
|
52
|
+
id: true,
|
|
53
|
+
email: true,
|
|
54
|
+
name: true,
|
|
55
|
+
role: true,
|
|
56
|
+
createdAt: true,
|
|
57
|
+
updatedAt: true,
|
|
58
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export async function updateMe(userId, data) {
|
|
63
|
+
return prisma.user.update({
|
|
64
|
+
where: { id: userId },
|
|
65
|
+
data: {
|
|
66
|
+
name: data.name ?? undefined,
|
|
67
|
+
profile: {
|
|
68
|
+
upsert: {
|
|
69
|
+
create: { bio: data.bio ?? null, avatarUrl: data.avatarUrl ?? null },
|
|
70
|
+
update: { bio: data.bio ?? null, avatarUrl: data.avatarUrl ?? null },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
select: {
|
|
75
|
+
id: true,
|
|
76
|
+
email: true,
|
|
77
|
+
name: true,
|
|
78
|
+
role: true,
|
|
79
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function adminCreateUser(input) {
|
|
84
|
+
const exists = await prisma.user.findUnique({ where: { email: input.email } });
|
|
85
|
+
if (exists)
|
|
86
|
+
throw new Error('EMAIL_TAKEN');
|
|
87
|
+
const passwordHash = await bcrypt.hash(input.password, Number(process.env.BCRYPT_SALT) || 10);
|
|
88
|
+
return prisma.user.create({
|
|
89
|
+
data: {
|
|
90
|
+
email: input.email,
|
|
91
|
+
passwordHash,
|
|
92
|
+
name: input.name ?? null,
|
|
93
|
+
role: input.role ?? 'USER',
|
|
94
|
+
profile: {
|
|
95
|
+
create: {
|
|
96
|
+
bio: input.bio ?? null,
|
|
97
|
+
avatarUrl: input.avatarUrl ?? null,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
select: {
|
|
102
|
+
id: true,
|
|
103
|
+
email: true,
|
|
104
|
+
name: true,
|
|
105
|
+
role: true,
|
|
106
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
107
|
+
createdAt: true,
|
|
108
|
+
updatedAt: true,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export async function adminUpdateUser(id, input) {
|
|
113
|
+
return prisma.user.update({
|
|
114
|
+
where: { id },
|
|
115
|
+
data: {
|
|
116
|
+
name: input.name ?? undefined,
|
|
117
|
+
role: input.role ?? undefined,
|
|
118
|
+
profile: {
|
|
119
|
+
upsert: {
|
|
120
|
+
create: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
|
|
121
|
+
update: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
select: {
|
|
126
|
+
id: true, email: true, name: true, role: true,
|
|
127
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
128
|
+
createdAt: true, updatedAt: true,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function adminDeleteUser(id) {
|
|
133
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
134
|
+
await prisma.user.delete({ where: { id } });
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type JwtPayload = {
|
|
2
|
+
sub: number;
|
|
3
|
+
role: "USER" | "ADMIN";
|
|
4
|
+
typ?: "refresh";
|
|
5
|
+
};
|
|
6
|
+
export declare function signAccessToken(payload: JwtPayload): string;
|
|
7
|
+
export declare function signRefreshToken(payload: JwtPayload): string;
|
|
8
|
+
export declare function verifyToken<T = JwtPayload>(token: string): T;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import { JWT_ACCESS_EXPIRES_IN, JWT_REFRESH_EXPIRES_IN, JWT_SECRET } from "../config/env.js";
|
|
3
|
+
export function signAccessToken(payload) {
|
|
4
|
+
return jwt.sign(payload, JWT_SECRET, {
|
|
5
|
+
expiresIn: JWT_ACCESS_EXPIRES_IN,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export function signRefreshToken(payload) {
|
|
9
|
+
return jwt.sign({ ...payload, typ: "refresh" }, JWT_SECRET, {
|
|
10
|
+
expiresIn: JWT_REFRESH_EXPIRES_IN,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function verifyToken(token) {
|
|
14
|
+
return jwt.verify(token, JWT_SECRET);
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-crud-lib",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Libreria CRUD modulare (Auth/User/Profile) con Prisma + TS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"prisma/schema.prisma"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.17"
|
|
21
|
+
},
|
|
22
|
+
"prisma": {
|
|
23
|
+
"schema": "prisma/schema.prisma"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"start": "node dist/index.js",
|
|
28
|
+
"dev": "node --loader ts-node/esm --no-warnings=ExperimentalWarning src/dev.ts",
|
|
29
|
+
"dev:db:up": "docker-compose up -d",
|
|
30
|
+
"dev:db:down": "docker-compose down -v",
|
|
31
|
+
"prisma:generate": "prisma generate",
|
|
32
|
+
"prisma:migrate": "prisma migrate dev",
|
|
33
|
+
"prisma:studio": "prisma studio",
|
|
34
|
+
"postinstall": "prisma generate",
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
36
|
+
"db:schema": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/schema.sql",
|
|
37
|
+
"db:seed": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/seed.sql",
|
|
38
|
+
"db:init": "npm run db:schema && npm run db:seed",
|
|
39
|
+
"generate:secret": "node scripts/generateSecret.js"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bcryptjs": "^2.4.6",
|
|
43
|
+
"@types/cors": "^2.8.19",
|
|
44
|
+
"@types/express": "^5.0.3",
|
|
45
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
46
|
+
"@types/node": "^24.3.0",
|
|
47
|
+
"prisma": "^6.14.0",
|
|
48
|
+
"ts-node": "^10.9.2",
|
|
49
|
+
"typescript": "^5.9.2"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@prisma/client": "^6.14.0",
|
|
53
|
+
"bcryptjs": "^2.4.3",
|
|
54
|
+
"dotenv": "^16.3.1",
|
|
55
|
+
"jsonwebtoken": "^9.0.2",
|
|
56
|
+
"zod": "^3.23.8"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"body-parser": "^1.20.2",
|
|
60
|
+
"cors": "^2.8.5",
|
|
61
|
+
"express": "^4.18.2"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -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
|
+
}
|