create-simo-api 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +33 -2
  2. package/index.js +318 -0
  3. package/package.json +24 -4
  4. package/template/README.md +61 -0
  5. package/template/_env.example +15 -0
  6. package/template/_gitignore +4 -0
  7. package/template/app.js +29 -0
  8. package/template/config/db.js +6 -0
  9. package/template/config/env.js +27 -0
  10. package/template/config/permissions.js +15 -0
  11. package/template/controllers/authController.js +53 -0
  12. package/template/controllers/productController.js +34 -0
  13. package/template/controllers/userController.js +22 -0
  14. package/template/index.js +6 -0
  15. package/template/middlewares/authMiddleware.js +15 -0
  16. package/template/middlewares/errorMiddleware.js +33 -0
  17. package/template/middlewares/rateLimitMiddleware.js +14 -0
  18. package/template/middlewares/roleMiddleware.js +22 -0
  19. package/template/middlewares/validateMiddleware.js +18 -0
  20. package/template/migrations/1792673478177_create-users-table.sql +16 -0
  21. package/template/migrations/1792842956065_create-refresh-tokens-table.sql +17 -0
  22. package/template/migrations/1792842957555_create-products-table.sql +14 -0
  23. package/template/models/productModel.js +42 -0
  24. package/template/models/refreshTokenModel.js +25 -0
  25. package/template/models/userModel.js +39 -0
  26. package/template/package.json +33 -0
  27. package/template/routes/authRoutes.js +13 -0
  28. package/template/routes/index.js +15 -0
  29. package/template/routes/productRoutes.js +17 -0
  30. package/template/routes/userRoutes.js +15 -0
  31. package/template/seeds/seed.js +50 -0
  32. package/template/services/authService.js +94 -0
  33. package/template/services/productService.js +37 -0
  34. package/template/services/userService.js +24 -0
  35. package/template/tests/api.test.js +174 -0
  36. package/template/utils/AppError.js +10 -0
  37. package/template/utils/verifyToken.js +12 -0
  38. package/template/validations/authValidation.js +18 -0
  39. package/template/validations/productValidation.js +21 -0
  40. package/template/validations/userValidation.js +10 -0
package/README.md CHANGED
@@ -1,3 +1,34 @@
1
- # Temporary Holding Version
1
+ # create-simo-api
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scaffold an Express 5 + PostgreSQL API pre-configured with:
4
+
5
+ - **JWT auth**: access token + refresh token in an `httpOnly` cookie, stored hashed, rotated on every refresh, reuse detection
6
+ - **Role permissions** with `authorize('PERMISSION')`
7
+ - **Zod** validation for request bodies, params and `.env`
8
+ - **Service layer** (routes → controllers → services → models)
9
+ - Central **error handling** (`AppError`, JSON 404, PostgreSQL errors)
10
+ - **Migrations** (`node-pg-migrate`), admin **seed**, **rate limiting**, **morgan** logs
11
+ - **Tests** with Supertest
12
+ - Example resources: users and products
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ npm create simo-api@latest my-api
18
+ cd my-api
19
+ # set DATABASE_URL in .env
20
+ npm run migrate:up
21
+ npm run seed
22
+ npm run dev
23
+ ```
24
+
25
+ `.env` is generated with random JWT secrets and a random admin password.
26
+
27
+ ## Requirements
28
+
29
+ - Node.js 20+
30
+ - PostgreSQL
31
+
32
+ ## License
33
+
34
+ MIT
package/index.js ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
3
+ import { dirname, join, relative, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { spawn } from 'node:child_process'
6
+ import { randomBytes } from 'node:crypto'
7
+ import { styleText } from 'node:util'
8
+ import { createInterface } from 'node:readline/promises'
9
+ import { createRequire } from 'node:module'
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url))
12
+ const TEMPLATE_DIR = join(__dirname, 'template')
13
+
14
+ const RENAME = {
15
+ _gitignore: '.gitignore',
16
+ '_env.example': '.env.example',
17
+ }
18
+
19
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
20
+
21
+ let createdDir = null
22
+
23
+ function removeProject() {
24
+ if (!createdDir) return
25
+ rmSync(createdDir, { recursive: true, force: true })
26
+ console.error(styleText('gray', ` Project folder removed: ${relative(process.cwd(), createdDir)}\n`))
27
+ createdDir = null
28
+ }
29
+
30
+ function fail(msg) {
31
+ console.error(`\n${styleText(['red', 'bold'], '✖')} ${styleText('red', msg)}\n`)
32
+ removeProject()
33
+ process.exit(1)
34
+ }
35
+
36
+ function cancel() {
37
+ console.log(`\n\n${styleText('gray', 'Cancelled.')}\n`)
38
+ removeProject()
39
+ process.exit(130)
40
+ }
41
+
42
+ process.on('SIGINT', cancel)
43
+
44
+ function listFiles(dir) {
45
+ return readdirSync(dir, { recursive: true })
46
+ .map((file) => join(dir, file))
47
+ .filter((file) => statSync(file).isFile())
48
+ .sort()
49
+ }
50
+
51
+ function installDependencies(cwd) {
52
+ return new Promise((resolvePromise) => {
53
+ let frame = 0
54
+ const label = 'Installation in progress... ☕'
55
+ const timer = setInterval(() => {
56
+ process.stdout.write(`\r${styleText('cyan', SPINNER[frame++ % SPINNER.length])} ${label}`)
57
+ }, 80)
58
+
59
+ let output = ''
60
+ const child = spawn('npm', ['install'], { cwd })
61
+ child.stdout.on('data', (data) => { output += data })
62
+ child.stderr.on('data', (data) => { output += data })
63
+
64
+ child.on('close', (code) => {
65
+ clearInterval(timer)
66
+ const icon = code === 0 ? styleText('green', '✔') : styleText('red', '✖')
67
+ process.stdout.write(`\r${icon} ${label}\n`)
68
+ resolvePromise({ code, output })
69
+ })
70
+ })
71
+ }
72
+
73
+ function runCommand(cwd, args) {
74
+ console.log(`\n${styleText('gray', '$')} ${styleText('cyan', `npm ${args.join(' ')}`)}`)
75
+ return new Promise((resolvePromise) => {
76
+ const child = spawn('npm', args, { cwd, stdio: 'inherit' })
77
+ child.on('close', (code) => resolvePromise(code))
78
+ })
79
+ }
80
+
81
+ async function ask(question) {
82
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
83
+ rl.on('SIGINT', cancel)
84
+ try {
85
+ const answer = await rl.question(question)
86
+ return answer.trim()
87
+ } catch {
88
+ cancel()
89
+ } finally {
90
+ rl.close()
91
+ }
92
+ }
93
+
94
+ const validators = {
95
+ identifier: (value) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)
96
+ ? null
97
+ : 'Only letters, numbers and _ are allowed, and it cannot start with a number.',
98
+ host: (value) => /^[^\s/:@]+$/.test(value) ? null : 'Enter a valid host, for example localhost.',
99
+ port: (value) => {
100
+ const port = Number(value)
101
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? null : 'Enter a number between 1 and 65535.'
102
+ },
103
+ }
104
+
105
+ async function askWithDefault(label, defaultValue, validate) {
106
+ const hint = defaultValue ? styleText('gray', ` (${defaultValue})`) : ''
107
+
108
+ while (true) {
109
+ const value = (await ask(`${styleText('bold', label)}${hint}: `)) || defaultValue
110
+ const error = validate?.(value)
111
+ if (!error) return value
112
+ console.log(` ${styleText('red', '✖')} ${styleText('red', error)}`)
113
+ }
114
+ }
115
+
116
+ function printRetry(message) {
117
+ console.log(`\n${styleText('red', '✖')} ${styleText('red', message)} ${styleText('gray', 'Try again.')}\n`)
118
+ }
119
+
120
+ async function askConnection(previous) {
121
+ return {
122
+ user: await askWithDefault('User', previous.user, validators.identifier),
123
+ password: await askWithDefault('Password', ''),
124
+ host: await askWithDefault('Host', previous.host, validators.host),
125
+ port: Number(await askWithDefault('Port', String(previous.port), validators.port)),
126
+ }
127
+ }
128
+
129
+ async function createDatabase(targetDir, db) {
130
+ const pg = createRequire(join(targetDir, 'package.json'))('pg')
131
+ const client = new pg.Client({ ...db, database: 'postgres' })
132
+
133
+ try {
134
+ await client.connect()
135
+ const { rowCount } = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [db.database])
136
+ if (rowCount > 0) return 'exists'
137
+ await client.query(`CREATE DATABASE "${db.database}"`)
138
+ return 'created'
139
+ } catch (error) {
140
+ if (error.code === '42P04') return 'exists'
141
+ if (error.code === '42501') return 'denied'
142
+ if (error.code === '28P01' || error.code === '28000') return 'auth'
143
+ if (['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN'].includes(error.code)) return 'unreachable'
144
+ fail(`Could not create the database: ${error.message}`)
145
+ } finally {
146
+ await client.end().catch(() => {})
147
+ }
148
+ }
149
+
150
+ async function createDatabaseAsAdmin(targetDir, db) {
151
+ console.log(`\n${styleText('gray', 'Enter a PostgreSQL admin account (the one created when PostgreSQL was installed).')}`)
152
+ const pg = createRequire(join(targetDir, 'package.json'))('pg')
153
+ let adminUser = 'postgres'
154
+
155
+ while (true) {
156
+ adminUser = await askWithDefault('Admin user', adminUser, validators.identifier)
157
+ const password = await askWithDefault('Admin password', '')
158
+ const client = new pg.Client({ host: db.host, port: db.port, database: 'postgres', user: adminUser, password })
159
+
160
+ try {
161
+ await client.connect()
162
+ await client.query(`CREATE DATABASE "${db.database}" OWNER "${db.user}"`)
163
+ return
164
+ } catch (error) {
165
+ if (error.code === '28P01' || error.code === '28000') printRetry(`Wrong password for PostgreSQL admin "${adminUser}".`)
166
+ else if (error.code === '42501') printRetry(`"${adminUser}" is not allowed to create databases either.`)
167
+ else fail(`Could not create the database: ${error.message}`)
168
+ } finally {
169
+ await client.end().catch(() => {})
170
+ }
171
+ }
172
+ }
173
+
174
+ function createDatabaseWithSudo(db) {
175
+ console.log(`\n${styleText('gray', '$')} ${styleText('cyan', `sudo -u postgres psql -c 'CREATE DATABASE "${db.database}" OWNER "${db.user}";'`)}`)
176
+ return new Promise((resolvePromise) => {
177
+ const child = spawn('sudo', ['-u', 'postgres', 'psql', '-c', `CREATE DATABASE "${db.database}" OWNER "${db.user}";`], { stdio: 'inherit' })
178
+ child.on('close', (code) => resolvePromise(code))
179
+ })
180
+ }
181
+
182
+ async function setupDatabase(targetDir, projectName) {
183
+ console.log(`\n${styleText('magenta', '🐘')} PostgreSQL setup\n`)
184
+
185
+ const defaultName = projectName.toLowerCase().replace(/[^a-z0-9_]/g, '_')
186
+ const database = await askWithDefault('Database name', defaultName, validators.identifier)
187
+
188
+ let db = { database, user: 'postgres', host: 'localhost', port: 5432 }
189
+ let status
190
+
191
+ while (true) {
192
+ db = { database, ...(await askConnection(db)) }
193
+ status = await createDatabase(targetDir, db)
194
+
195
+ if (status === 'auth') printRetry(`Wrong user or password for "${db.user}".`)
196
+ else if (status === 'unreachable') printRetry(`Cannot reach PostgreSQL on ${db.host}:${db.port}. Is it running?`)
197
+ else break
198
+ }
199
+
200
+ if (status === 'denied') {
201
+ console.log(`\n${styleText('yellow', '⚠')} User "${db.user}" is not allowed to create databases.`)
202
+
203
+ const useSudo = process.platform === 'linux'
204
+ && ['', 'y', 'yes'].includes((await ask(`${styleText('yellow', '?')} Create it with the "postgres" system user (sudo)? ${styleText('gray', '(Y/n)')} `)).toLowerCase())
205
+
206
+ const createdWithSudo = useSudo && (await createDatabaseWithSudo(db)) === 0
207
+
208
+ if (!createdWithSudo) {
209
+ await createDatabaseAsAdmin(targetDir, db)
210
+ }
211
+ status = 'created'
212
+ }
213
+
214
+ const url = `postgres://${encodeURIComponent(db.user)}:${encodeURIComponent(db.password)}@${db.host}:${db.port}/${db.database}`
215
+ const envPath = join(targetDir, '.env')
216
+ writeFileSync(envPath, readFileSync(envPath, 'utf8').replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${url}`))
217
+
218
+ const maskedUrl = url.replace(`:${encodeURIComponent(db.password)}@`, ':****@')
219
+ console.log(`
220
+ ${styleText('green', '✔')} Database ${styleText('cyan', db.database)} ${status === 'exists' ? 'already exists, using it' : 'created'}
221
+
222
+ ${styleText('gray', 'Host ')} ${db.host}:${db.port}
223
+ ${styleText('gray', 'Database ')} ${db.database}
224
+ ${styleText('gray', 'User ')} ${db.user}
225
+ ${styleText('gray', 'URL ')} ${maskedUrl}
226
+ ${styleText('gray', 'Saved in ')} .env`)
227
+ }
228
+
229
+ async function runSetup(targetDir, projectName) {
230
+ await setupDatabase(targetDir, projectName)
231
+
232
+ for (const args of [['run', 'migrate:up'], ['run', 'seed']]) {
233
+ const code = await runCommand(targetDir, args)
234
+ if (code !== 0) {
235
+ fail(`"npm ${args.join(' ')}" failed. Fix the error above, then create the project again.`)
236
+ }
237
+ }
238
+
239
+ createdDir = null
240
+ await runCommand(targetDir, ['run', 'dev'])
241
+ }
242
+
243
+ async function main() {
244
+ const projectName = process.argv[2]
245
+
246
+ if (!projectName) {
247
+ fail('Usage: npm create simo-api@latest <project-name>')
248
+ }
249
+
250
+ const targetDir = resolve(process.cwd(), projectName)
251
+
252
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
253
+ fail(`Directory "${projectName}" already exists and is not empty.`)
254
+ }
255
+
256
+ console.log(`\n${styleText('yellow', '⚡')} We will scaffold your app in a few seconds..\n`)
257
+
258
+ mkdirSync(targetDir, { recursive: true })
259
+ createdDir = targetDir
260
+ cpSync(TEMPLATE_DIR, targetDir, { recursive: true })
261
+
262
+ for (const [from, to] of Object.entries(RENAME)) {
263
+ const src = join(targetDir, from)
264
+ if (existsSync(src)) renameSync(src, join(targetDir, to))
265
+ }
266
+
267
+ const pkgPath = join(targetDir, 'package.json')
268
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
269
+ pkg.name = projectName
270
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
271
+
272
+ const env = readFileSync(join(targetDir, '.env.example'), 'utf8')
273
+ .replace(/^ACCESS_TOKEN_SECRET=$/m, `ACCESS_TOKEN_SECRET=${randomBytes(64).toString('hex')}`)
274
+ .replace(/^REFRESH_TOKEN_SECRET=$/m, `REFRESH_TOKEN_SECRET=${randomBytes(64).toString('hex')}`)
275
+ .replace(/^ADMIN_PASSWORD=$/m, `ADMIN_PASSWORD=${randomBytes(12).toString('base64url')}`)
276
+ writeFileSync(join(targetDir, '.env'), env)
277
+
278
+ for (const file of listFiles(targetDir)) {
279
+ const path = relative(process.cwd(), file)
280
+ console.log(`${styleText('green', 'CREATE')} ${path} ${styleText('gray', `(${statSync(file).size} bytes)`)}`)
281
+ }
282
+
283
+ console.log()
284
+ const install = await installDependencies(targetDir)
285
+
286
+ if (install.code !== 0) {
287
+ console.error(styleText('gray', install.output))
288
+ fail('Dependency installation failed. Check your internet connection and npm, then try again.')
289
+ }
290
+
291
+ console.log(`
292
+ ${styleText('magenta', '🚀')} Successfully created project ${styleText('cyan', projectName)}
293
+ ${styleText('green', ' Welcome to Simo Express App. Happy coding!')}
294
+
295
+ 👉 Get started with the following commands:
296
+
297
+ ${styleText('gray', '$')} ${styleText('cyan', `cd ${projectName}`)}
298
+ ${styleText('gray', '$ # set DATABASE_URL in .env')}
299
+ ${styleText('gray', '$')} ${styleText('cyan', 'npm run migrate:up')}
300
+ ${styleText('gray', '$')} ${styleText('cyan', 'npm run seed')}
301
+ ${styleText('gray', '$')} ${styleText('cyan', 'npm run dev')}
302
+ `)
303
+
304
+ if (!process.stdin.isTTY) {
305
+ createdDir = null
306
+ return
307
+ }
308
+
309
+ const answer = await ask(`${styleText('yellow', '?')} Do you want me to run these commands for you? ${styleText('gray', '(Y/n)')} `)
310
+
311
+ if (answer === '' || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
312
+ await runSetup(targetDir, projectName)
313
+ } else {
314
+ createdDir = null
315
+ }
316
+ }
317
+
318
+ main()
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "create-simo-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "Scaffold an Express 5 + PostgreSQL API with JWT auth (access + refresh rotation), role permissions, Zod validation, service layer, migrations, seed and tests.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-simo-api": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "template"
12
+ ],
13
+ "keywords": [
14
+ "express",
15
+ "postgresql",
16
+ "jwt",
17
+ "api",
18
+ "boilerplate",
19
+ "scaffold",
20
+ "create"
21
+ ],
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=20"
25
+ }
26
+ }
@@ -0,0 +1,61 @@
1
+ # Simo API
2
+
3
+ API Express 5 + PostgreSQL.
4
+
5
+ ## Démarrage
6
+
7
+ ```bash
8
+ npm install
9
+ cp .env.example .env # si .env n'existe pas, puis remplir DATABASE_URL
10
+ npm run migrate:up
11
+ npm run seed # crée l'admin du .env
12
+ npm run dev
13
+ ```
14
+
15
+ Générer un secret JWT :
16
+
17
+ ```bash
18
+ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
19
+ ```
20
+
21
+ ## Structure
22
+
23
+ ```
24
+ app.js app Express (middlewares, routes)
25
+ index.js lance le serveur
26
+ config/ env (validé avec Zod), db, permissions
27
+ routes/ auth, user, product
28
+ controllers/ req/res seulement
29
+ services/ logique métier
30
+ models/ requêtes SQL
31
+ middlewares/ auth, authorize, validate, rateLimit, errors
32
+ validations/ schémas Zod
33
+ utils/ AppError, verifyToken
34
+ migrations/
35
+ seeds/
36
+ tests/
37
+ ```
38
+
39
+ ## Routes
40
+
41
+ | Route | Accès |
42
+ |---|---|
43
+ | `POST /api/auth/register` · `login` | public |
44
+ | `POST /api/auth/refresh` · `logout` | cookie `refreshToken` |
45
+ | `GET /api/user/me` | connecté |
46
+ | `GET /api/user/getAll` | admin |
47
+ | `POST /api/user/create` | connecté (seul l'admin choisit le rôle) |
48
+ | `GET /api/product/getAll` · `/:id` | public |
49
+ | `POST /api/product/create` | admin |
50
+ | `PATCH /api/product/update/:id` | admin |
51
+ | `DELETE /api/product/delete/:id` | admin |
52
+
53
+ ## Fonctionnement
54
+
55
+ - **Auth** : access token JWT (`ACCESS_TOKEN_EXPIRES_IN`, 15m par défaut) dans `Authorization: Bearer`, refresh token (`REFRESH_TOKEN_EXPIRES_DAYS`, 7 j par défaut) en cookie `httpOnly`, stocké hashé dans `refresh_tokens`. Rotation à chaque refresh ; un token réutilisé révoque toutes les sessions de l'utilisateur.
56
+ - **Permissions** : `authorize('PERMISSION')` sur la route, rôles autorisés dans `config/permissions.js`.
57
+ - **Validation** : `validate(schema)` pour le body, `validate(schema, 'params')` pour les paramètres.
58
+ - **Erreurs** : `throw new AppError(message, status)` dans les controllers (pas de try/catch). `errorHandler` renvoie du JSON, convertit les erreurs PostgreSQL (doublon → 409) et renvoie 404 pour les routes inconnues.
59
+ - **Env** : le serveur refuse de démarrer si une variable manque ou est invalide.
60
+ - **Logs** : morgan (`dev` en développement, `combined` en production).
61
+ - **Rate limit** : 100 requêtes / 15 min par IP sur `/api`.
@@ -0,0 +1,15 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+
4
+ DATABASE_URL=postgres://user:password@localhost:5432/dbname
5
+
6
+ CORS_ORIGIN=http://localhost:5173
7
+
8
+ ACCESS_TOKEN_SECRET=
9
+ REFRESH_TOKEN_SECRET=
10
+ ACCESS_TOKEN_EXPIRES_IN=15m
11
+ REFRESH_TOKEN_EXPIRES_DAYS=7
12
+
13
+ ADMIN_NAME=Admin
14
+ ADMIN_EMAIL=admin@example.com
15
+ ADMIN_PASSWORD=
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ .env
3
+ *.log
4
+ coverage
@@ -0,0 +1,29 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import cookieParser from 'cookie-parser';
4
+ import morgan from 'morgan';
5
+ import env from './config/env.js';
6
+ import routes from './routes/index.js';
7
+ import { notFound, errorHandler } from './middlewares/errorMiddleware.js';
8
+ import { apiLimiter } from './middlewares/rateLimitMiddleware.js';
9
+
10
+ const app = express();
11
+
12
+ if (env.NODE_ENV !== 'test') {
13
+ app.use(morgan(env.NODE_ENV === 'production' ? 'combined' : 'dev'));
14
+ }
15
+ app.use(express.json());
16
+ app.use(cookieParser());
17
+
18
+ app.use(cors({ origin: env.CORS_ORIGIN, credentials: true }));
19
+
20
+ app.get('/', (req, res) => {
21
+ res.send('API is running...');
22
+ });
23
+
24
+ app.use('/api', apiLimiter, routes);
25
+
26
+ app.use(notFound);
27
+ app.use(errorHandler);
28
+
29
+ export default app;
@@ -0,0 +1,6 @@
1
+ import pg from 'pg';
2
+ import env from './env.js';
3
+
4
+ const { Pool } = pg;
5
+
6
+ export default new Pool({ connectionString: env.DATABASE_URL });
@@ -0,0 +1,27 @@
1
+ import 'dotenv/config';
2
+ import { z } from 'zod';
3
+
4
+ const envSchema = z.object({
5
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
6
+ PORT: z.coerce.number().int().positive().default(3000),
7
+ DATABASE_URL: z.url(),
8
+ ACCESS_TOKEN_SECRET: z.string().min(32),
9
+ REFRESH_TOKEN_SECRET: z.string().min(32),
10
+ ACCESS_TOKEN_EXPIRES_IN: z.string().regex(/^\d+(ms|s|m|h|d)$/, 'Use a duration like 15m, 1h or 1d').default('15m'),
11
+ REFRESH_TOKEN_EXPIRES_DAYS: z.coerce.number().int().positive().default(7),
12
+ CORS_ORIGIN: z.string().min(1).transform((value) => value.split(',').map((origin) => origin.trim())),
13
+ });
14
+
15
+ const result = envSchema.safeParse(process.env);
16
+
17
+ if (!result.success) {
18
+ console.error('Invalid environment variables:');
19
+ for (const issue of result.error.issues) {
20
+ console.error(` - ${issue.path.join('.')}: ${issue.message}`);
21
+ }
22
+ process.exit(1);
23
+ }
24
+
25
+ const env = result.data;
26
+
27
+ export default env;
@@ -0,0 +1,15 @@
1
+ export const ROLES = {
2
+ ADMIN: 'admin',
3
+ USER: 'user'
4
+ };
5
+
6
+
7
+ export const PERMISSIONS = {
8
+ USER_CREATE: [ROLES.ADMIN, ROLES.USER],
9
+ USER_DELETE: [ROLES.ADMIN],
10
+ USER_GETALL: [ROLES.ADMIN],
11
+ PRODUCT_CREATE: [ROLES.ADMIN],
12
+ PRODUCT_UPDATE: [ROLES.ADMIN],
13
+ PRODUCT_DELETE: [ROLES.ADMIN],
14
+ PRODUCT_GETALL: [ROLES.ADMIN, ROLES.USER]
15
+ }
@@ -0,0 +1,53 @@
1
+ import authService, { REFRESH_TOKEN_MAX_AGE } from '../services/authService.js';
2
+ import env from '../config/env.js';
3
+
4
+ const refreshCookieOptions = {
5
+ httpOnly: true,
6
+ secure: env.NODE_ENV === "production",
7
+ sameSite: "strict",
8
+ path: "/api/auth",
9
+ };
10
+
11
+ const sendRefreshCookie = (res, token) => {
12
+ res.cookie("refreshToken", token, {
13
+ ...refreshCookieOptions,
14
+ maxAge: REFRESH_TOKEN_MAX_AGE
15
+ });
16
+ };
17
+
18
+ const login = async (req, res) => {
19
+ const { accessToken, refreshToken, user } = await authService.login(req.body.email, req.body.password);
20
+ sendRefreshCookie(res, refreshToken);
21
+ res.status(200).json({ message: 'Login successful', accessToken, user });
22
+ };
23
+
24
+ const register = async (req, res) => {
25
+ const userId = await authService.register(req.body);
26
+ res.status(201).json({ message: 'User registered successfully', userId });
27
+ };
28
+
29
+ const refresh = async (req, res) => {
30
+ const { accessToken, refreshToken } = await authService.refresh(req.cookies.refreshToken);
31
+ sendRefreshCookie(res, refreshToken);
32
+ res.status(200).json({ accessToken });
33
+ };
34
+
35
+ const clearRefreshCookieOnError = (err, req, res, next) => {
36
+ res.clearCookie("refreshToken", refreshCookieOptions);
37
+ next(err);
38
+ };
39
+
40
+ const logout = async (req, res) => {
41
+ await authService.logout(req.cookies.refreshToken);
42
+ res.clearCookie("refreshToken", refreshCookieOptions);
43
+ res.status(200).json({ message: 'Logged out' });
44
+ };
45
+
46
+
47
+ export default {
48
+ login,
49
+ register,
50
+ refresh,
51
+ clearRefreshCookieOnError,
52
+ logout
53
+ };
@@ -0,0 +1,34 @@
1
+ import productService from '../services/productService.js';
2
+
3
+ const getAllProducts = async (req, res) => {
4
+ const products = await productService.getAll();
5
+ res.status(200).json({ products });
6
+ };
7
+
8
+ const getProductById = async (req, res) => {
9
+ const product = await productService.getById(req.params.id);
10
+ res.status(200).json({ product });
11
+ };
12
+
13
+ const createProduct = async (req, res) => {
14
+ const product = await productService.create(req.body);
15
+ res.status(201).json({ message: 'Product created successfully', product });
16
+ };
17
+
18
+ const updateProduct = async (req, res) => {
19
+ const product = await productService.update(req.params.id, req.body);
20
+ res.status(200).json({ message: 'Product updated successfully', product });
21
+ };
22
+
23
+ const deleteProduct = async (req, res) => {
24
+ await productService.remove(req.params.id);
25
+ res.status(200).json({ message: 'Product deleted successfully' });
26
+ };
27
+
28
+ export default {
29
+ getAllProducts,
30
+ getProductById,
31
+ createProduct,
32
+ updateProduct,
33
+ deleteProduct
34
+ };
@@ -0,0 +1,22 @@
1
+ import userService from '../services/userService.js';
2
+
3
+ const getAllUsers = async (req, res) => {
4
+ const users = await userService.getAll();
5
+ res.status(200).json({ users });
6
+ };
7
+
8
+ const getMe = async (req, res) => {
9
+ const user = await userService.getById(req.user.id);
10
+ res.status(200).json({ user });
11
+ };
12
+
13
+ const createUser = async (req, res) => {
14
+ const userId = await userService.create(req.body, req.user);
15
+ res.status(201).json({ message: 'User created successfully', userId });
16
+ };
17
+
18
+ export default {
19
+ getAllUsers,
20
+ getMe,
21
+ createUser
22
+ };
@@ -0,0 +1,6 @@
1
+ import env from './config/env.js';
2
+ import app from './app.js';
3
+
4
+ app.listen(env.PORT, () => {
5
+ console.log(`Server is running on port ${env.PORT}`);
6
+ });