create-lyrajs 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/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2025 Matthieu Fergola
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ ---
20
+
21
+ CONTRIBUTOR LICENSE AGREEMENT
22
+
23
+ By submitting a contribution to this project, you agree that:
24
+
25
+ 1. You grant Matthieu Fergola (the project owner) a perpetual, worldwide,
26
+ non-exclusive, royalty-free, irrevocable license to use, reproduce,
27
+ modify, display, perform, sublicense, and distribute your contribution
28
+ under any license terms (including proprietary licenses).
29
+
30
+ 2. You grant Matthieu Fergola the right to relicense the entire codebase
31
+ (including your contributions) under different terms in the future.
32
+
33
+ 3. You certify that your contribution is your original work or you have
34
+ the right to submit it under these terms.
35
+
36
+ 4. You understand that your contributions will be publicly available
37
+ under GPL-3.0 to end users, but Matthieu Fergola retains the right
38
+ to use them in proprietary versions.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # create-lyrajs
2
+
3
+ [![npm version](https://img.shields.io/npm/v/create-lyrajs)](https://www.npmjs.com/package/create-lyrajs)
4
+ [![CLA Assistant](https://cla-assistant.io/readme/badge/devway-eu/lyrajs-template)](https://cla-assistant.io/devway-eu/lyrajs-template)
5
+ [![docs](https://img.shields.io/badge/docs-read-green)](https://lyrajs.dev)
6
+ [![Discord](https://img.shields.io/discord/1449345012604342427?label=discord&logo=discord&logoColor=white)](https://discord.gg/cWvUh8pQNU)
7
+
8
+ The official project template and scaffolding tool for LyraJS, a lightweight TypeScript framework for building robust APIs.
9
+
10
+ ## About
11
+
12
+ `create-lyrajs` is the quickest way to start a new LyraJS project. It's a CLI tool that scaffolds a fully-configured LyraJS application with best practices, pre-built authentication, and example code to get you started immediately.
13
+
14
+ ## Role in the Framework
15
+
16
+ This repository serves as the **project starter** for LyraJS. It provides:
17
+
18
+ - **CLI Tool** - Interactive project creation via `npm create lyrajs`
19
+ - **Project Template** - Pre-configured application structure with working examples
20
+ - **Best Practices** - Demonstrates recommended patterns and conventions
21
+ - **Starting Point** - Complete foundation for building production APIs
22
+
23
+ When developers run `npm create lyrajs`, this tool copies the template folder to create a new project with everything needed to start building an API.
24
+
25
+ ## What's Included in the Template
26
+
27
+ ### Pre-built Features
28
+
29
+ 1. **Authentication System**
30
+ - User registration and login
31
+ - JWT token generation (access + refresh tokens)
32
+ - Password hashing with bcrypt
33
+ - Protected routes via configuration
34
+
35
+ 2. **User Management**
36
+ - User entity with role support
37
+ - UserRepository with custom queries
38
+ - UserController with CRUD operations
39
+ - Complete user routes
40
+
41
+ 3. **Database Setup**
42
+ - MySQL/MariaDB configuration
43
+ - Migration system ready to use
44
+ - Example entity (User)
45
+ - Fixture system for seed data
46
+
47
+ 4. **Configuration Files**
48
+ - `database.yaml` - Database connection settings
49
+ - `security.yaml` - JWT and access control rules
50
+ - `router.yaml` - API base path configuration
51
+ - `parameters.yaml` - Application metadata
52
+ - `mailer.yaml` - Email service settings
53
+ - `.env` - Environment variables
54
+
55
+ 5. **Project Structure**
56
+ - TypeScript configuration
57
+ - ESLint and Prettier setup
58
+ - Organized folder structure (controller, entity, repository, router, etc.)
59
+ - Example code demonstrating patterns
60
+
61
+ 6. **Development Tools**
62
+ - Hot reload with `npm run dev`
63
+ - Build scripts
64
+ - Type definitions
65
+ - Path aliases configured
66
+
67
+ ### Project Structure
68
+
69
+ ```
70
+ my-project/
71
+ ├── src/
72
+ │ ├── controller/ # HTTP request handlers
73
+ │ │ ├── AuthController.ts # Registration, login, logout
74
+ │ │ └── UserController.ts # User CRUD operations
75
+ │ ├── entity/ # Database models
76
+ │ │ └── User.ts # User entity with decorators
77
+ │ ├── repository/ # Data access layer
78
+ │ │ └── UserRepository.ts # User database operations
79
+ │ ├── router/ # Route definitions
80
+ │ │ ├── index.ts # Main router setup
81
+ │ │ └── routes/
82
+ │ │ ├── authRoutes.ts # Auth endpoints
83
+ │ │ └── userRoutes.ts # User endpoints
84
+ │ ├── middleware/ # Custom middleware
85
+ │ │ └── YourMiddleware.ts # Example middleware
86
+ │ ├── services/ # Business logic services
87
+ │ │ └── YourService.ts # Example service
88
+ │ ├── fixtures/ # Seed data
89
+ │ │ └── AppFixtures.ts # Sample user data
90
+ │ ├── tests/ # Test files
91
+ │ │ └── exemple.test.ts # Example test
92
+ │ ├── types/ # TypeScript types
93
+ │ │ └── ExempleType.ts # Example types
94
+ │ └── server.ts # Application entry point
95
+ ├── config/ # YAML configuration files
96
+ │ ├── database.yaml
97
+ │ ├── router.yaml
98
+ │ ├── security.yaml
99
+ │ ├── parameters.yaml
100
+ │ └── mailer.yaml
101
+ ├── migrations/ # SQL migration files
102
+ ├── .env # Environment variables
103
+ ├── .prettierrc # Code formatting rules
104
+ ├── eslint.config.js # Linting configuration
105
+ ├── package.json # Dependencies and scripts
106
+ ├── tsconfig.json # TypeScript configuration
107
+ └── README.md # Project documentation
108
+ ```
109
+
110
+ ## Usage
111
+
112
+ ### Create a New Project
113
+
114
+ ```bash
115
+ # Using npm (recommended)
116
+ npm create lyrajs
117
+
118
+ # Using npx
119
+ npx create-lyrajs
120
+
121
+ # Using yarn
122
+ yarn create lyrajs
123
+ ```
124
+
125
+ The CLI will prompt you for a project name:
126
+
127
+ ```bash
128
+ Project name: my-api
129
+ ✅ Project "my-api" created.
130
+ ➡ cd my-api
131
+ ➡ npm install
132
+ ➡ npm run dev
133
+ ```
134
+
135
+ ### Next Steps After Creation
136
+
137
+ 1. **Configure environment variables** in `.env`:
138
+ ```env
139
+ DB_HOST=127.0.0.1
140
+ DB_PORT=3306
141
+ DB_USER=root
142
+ DB_PASSWORD=your_password
143
+ DB_NAME=my_database
144
+ JWT_SECRET=your_secret_key
145
+ ```
146
+
147
+ 2. **Create the database**:
148
+ ```bash
149
+ npx maestro create:database
150
+ ```
151
+
152
+ 3. **Run migrations**:
153
+ ```bash
154
+ npx maestro migration:migrate
155
+ ```
156
+
157
+ 4. **Load sample data** (optional):
158
+ ```bash
159
+ npx maestro fixtures:load
160
+ ```
161
+
162
+ 5. **Start developing**:
163
+ ```bash
164
+ npm run dev
165
+ ```
166
+
167
+ Your API will be available at `http://localhost:3333/api`
168
+
169
+ ### Pre-built Endpoints
170
+
171
+ The template includes these working endpoints:
172
+
173
+ **Authentication:**
174
+ - `POST /api/auth/sign-up` - User registration
175
+ - `POST /api/auth/sign-in` - User login
176
+ - `GET /api/auth/user` - Get authenticated user (protected)
177
+ - `GET /api/auth/sign-out` - Logout (protected)
178
+
179
+ **User Management:**
180
+ - `GET /api/user/all` - List all users
181
+ - `GET /api/user/:id` - Get user by ID
182
+ - `PUT /api/user/` - Create new user
183
+ - `PATCH /api/user/:id` - Update user
184
+ - `DELETE /api/user/:id` - Delete user
185
+
186
+ ## Contributing
187
+
188
+ We welcome contributions to improve the template! Here's how you can help:
189
+
190
+ ### Getting Started
191
+
192
+ 1. **Fork the repository**
193
+ ```bash
194
+ git clone https://github.com/your-username/lyrajs-template.git
195
+ cd lyrajs-template
196
+ ```
197
+
198
+ 2. **Test the CLI locally**
199
+ ```bash
200
+ npm link
201
+
202
+ # Create a test project
203
+ cd /path/to/test-location
204
+ create-lyrajs
205
+ ```
206
+
207
+ 3. **Make changes to the template**
208
+ - Edit files in the `template/` folder
209
+ - Update CLI logic in `src/createApp.js`
210
+ - Test thoroughly before submitting
211
+
212
+ ### Contribution Guidelines
213
+
214
+ - **Read the guidelines** - See [CONTRIBUTING.md](https://github.com/devway-eu/lyrajs/blob/main/lyrajs-core/CONTRIBUTING.md)
215
+ - **Maintain consistency** - Follow existing patterns and conventions
216
+ - **Test thoroughly** - Ensure the generated project works correctly
217
+ - **Document changes** - Update the template README if needed
218
+ - **Keep it simple** - The template should be easy to understand for beginners
219
+
220
+ ### Areas for Contribution
221
+
222
+ We particularly welcome contributions in these areas:
223
+
224
+ 1. **Template Improvements**
225
+ - Additional example code
226
+ - Better default configuration
227
+ - Improved project structure
228
+ - More comprehensive README in generated projects
229
+
230
+ 2. **Pre-built Features**
231
+ - Additional authentication methods
232
+ - Example middleware implementations
233
+ - Sample services and business logic
234
+ - Test examples
235
+
236
+ 3. **Developer Experience**
237
+ - Better error messages
238
+ - Improved CLI prompts
239
+ - Setup wizard enhancements
240
+ - Documentation improvements
241
+
242
+ 4. **Configuration**
243
+ - Environment-specific configs
244
+ - Docker setup
245
+ - CI/CD examples
246
+ - Deployment guides
247
+
248
+ 5. **Code Quality**
249
+ - ESLint rule improvements
250
+ - Prettier configuration
251
+ - TypeScript strictness
252
+ - Code comments and documentation
253
+
254
+ ### Pull Request Process
255
+
256
+ 1. **Create a feature branch**
257
+ ```bash
258
+ git checkout -b feature/improve-template
259
+ ```
260
+
261
+ 2. **Make your changes**
262
+ - Modify files in `template/` folder
263
+ - Update CLI if needed
264
+ - Test the generated project
265
+
266
+ 3. **Test the changes**
267
+ ```bash
268
+ npm link
269
+ cd /tmp
270
+ create-lyrajs
271
+ cd my-test-project
272
+ npm install
273
+ npm run dev
274
+ ```
275
+
276
+ 4. **Commit and push**
277
+ ```bash
278
+ git add .
279
+ git commit -m "Improve template with [description]"
280
+ git push origin feature/improve-template
281
+ ```
282
+
283
+ 5. **Open a Pull Request**
284
+ - Describe what you changed and why
285
+ - Include screenshots if UI-related
286
+ - Ensure the template generates a working project
287
+
288
+ ### Testing Checklist
289
+
290
+ Before submitting a PR, verify:
291
+
292
+ - ✅ Generated project installs without errors
293
+ - ✅ `npm run dev` starts successfully
294
+ - ✅ Authentication endpoints work
295
+ - ✅ Database migrations execute properly
296
+ - ✅ TypeScript compiles without errors
297
+ - ✅ ESLint passes
298
+ - ✅ All configuration files are valid
299
+ - ✅ README is clear and accurate
300
+
301
+ ### Reporting Issues
302
+
303
+ Found a problem with the template?
304
+
305
+ - **Check existing issues** - Search for similar problems first
306
+ - **Create a new issue** - Use the [issue tracker](https://github.com/devway-eu/lyrajs-template/issues)
307
+ - **Provide details** - Include:
308
+ - Node.js and npm versions
309
+ - Operating system
310
+ - Steps to reproduce
311
+ - Expected vs actual behavior
312
+ - Error messages or logs
313
+
314
+ ## Template Customization
315
+
316
+ When creating your own LyraJS project, you can customize the template by:
317
+
318
+ 1. **Modifying entities** - Add your own entities and relationships
319
+ 2. **Adjusting routes** - Change API endpoints and structure
320
+ 3. **Customizing authentication** - Adapt to your security requirements
321
+ 4. **Adding features** - Extend with your own controllers and services
322
+ 5. **Updating configuration** - Adjust settings for your environment
323
+
324
+ ## Links
325
+
326
+ - **GitHub Repository**: [github.com/devway-eu/lyrajs-template](https://github.com/devway-eu/lyrajs-template)
327
+ - **npm Package**: [npmjs.com/package/create-lyrajs](https://www.npmjs.com/package/create-lyrajs)
328
+ - **Main Repository**: [github.com/devway-eu/lyrajs](https://github.com/devway-eu/lyrajs)
329
+ - **Core Package**: [github.com/devway-eu/lyrajs-core](https://github.com/devway-eu/lyrajs-core)
330
+ - **Documentation**: [lyrajs.dev](https://lyrajs.dev)
331
+
332
+ ## License
333
+
334
+ LyraJS is licensed under the [GPL-3.0 License](./LICENSE).
335
+
336
+ ## Authors
337
+
338
+ - **Matthieu Fergola** - Core Developer
339
+ - **Anthony Dewitte** - Core Developer
340
+
341
+ ## Acknowledgments
342
+
343
+ Built with dedication by the Devway team and our amazing contributors.
344
+
345
+ ---
346
+
347
+ **Need help?** Check the [documentation](https://lyrajs.dev) or join our [Discussions](https://github.com/devway-eu/lyrajs/discussions).
348
+
349
+ ## Contributors ❤️
350
+
351
+ ![Contributors](https://img.shields.io/github/contributors/devway-eu/lyrajs)
package/cli.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createApp } from './src/createApp.js'
4
+ await createApp()
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "create-lyrajs",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to create new LyraJS projects",
5
+ "keywords": ["create", "lyrajs", "framework", "NodeJS", "api", "typescript", "cli", "scaffold"],
6
+ "author": "Matthieu Fergola & Anthony Dewitte",
7
+ "license": "GPL-3.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/devway-eu/lyrajs-template"
11
+ },
12
+ "bin": {
13
+ "create-lyrajs": "cli.js"
14
+ },
15
+ "type": "module",
16
+ "files": [
17
+ "cli.js",
18
+ "src",
19
+ "template",
20
+ "template/.gitignore"
21
+ ]
22
+ }
@@ -0,0 +1,107 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import { fileURLToPath } from "url"
4
+ import { mkdir, cp } from "fs/promises"
5
+ import readline from "readline"
6
+ import { spawn } from "child_process"
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ export async function createApp() {
11
+ const projectName = await prompt("Project name: ")
12
+
13
+ const targetDir = path.resolve(process.cwd(), projectName)
14
+ const templateDir = path.resolve(__dirname, "../template")
15
+
16
+ if (fs.existsSync(targetDir)) {
17
+ console.error(`❌ Directory ${projectName} already exists.`)
18
+ process.exit(1)
19
+ }
20
+
21
+ console.log(`\nCreating project "${projectName}"...`)
22
+
23
+ await mkdir(targetDir)
24
+ await cp(templateDir, targetDir, { recursive: true })
25
+
26
+ const envExample = path.join(targetDir, '.env.example')
27
+ if (fs.existsSync(envExample)) {
28
+ await cp(envExample, path.join(targetDir, '.env'))
29
+ }
30
+
31
+ console.log(`\n✔ Project "${projectName}" created.`)
32
+
33
+ const shouldInstall = await confirm("\nInstall dependencies now? (Y/n) ")
34
+
35
+ if(shouldInstall)
36
+ {
37
+ console.log(`\n➟ Installing dependencies...`)
38
+
39
+ const installSuccess = await installDependencies(targetDir)
40
+
41
+ if(installSuccess)
42
+ {
43
+ console.log(`\n✔ Dependencies installed successfully!\n`)
44
+ console.log(`You're all set! Run these commands to start:\n`)
45
+ console.log(`➡ cd ${projectName}`)
46
+ console.log(`➡ npm run dev`)
47
+ }
48
+ else
49
+ {
50
+ console.log(`➡ cd ${projectName}`)
51
+ console.log(`➡ npm install`)
52
+ console.log(`➡ npm run dev`)
53
+ }
54
+ }
55
+ else
56
+ {
57
+ console.log(`➡ cd ${projectName}`)
58
+ console.log(`➡ npm install`)
59
+ console.log(`➡ npm run dev`)
60
+ }
61
+ }
62
+
63
+ // Simple readline prompt
64
+ function prompt(question) {
65
+ const rl = readline.createInterface({
66
+ input: process.stdin,
67
+ output: process.stdout
68
+ })
69
+ return new Promise((resolve) => rl.question(question, (ans) => {
70
+ rl.close()
71
+ resolve(ans.trim())
72
+ }))
73
+ }
74
+
75
+ function installDependencies(targetDir) {
76
+ return new Promise((resolve) => {
77
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"
78
+ const install = spawn(npmCommand, ["install"], {
79
+ cwd: targetDir,
80
+ stdio: "inherit",
81
+ shell: true
82
+ })
83
+
84
+ install.on("close", (code) => {
85
+ resolve(code === 0)
86
+ })
87
+
88
+ install.on("error", () => {
89
+ resolve(false)
90
+ })
91
+ })
92
+ }
93
+
94
+ function confirm(question) {
95
+ const rl = readline.createInterface({
96
+ input: process.stdin,
97
+ output: process.stdout
98
+ })
99
+ return new Promise((resolve) => {
100
+ rl.question(question, (ans) => {
101
+ rl.close()
102
+ const answer = ans.trim().toLowerCase()
103
+ // Default to yes if empty or starts with 'y'
104
+ resolve(answer === "" || answer === "y" || answer === "yes")
105
+ })
106
+ })
107
+ }
@@ -0,0 +1,30 @@
1
+ # Node environment
2
+ NODE_ENV=dev
3
+ # Time zone
4
+ TZ=Europe/Paris
5
+ # Limits
6
+ REQUEST_MAX_SIZE=10mb
7
+ # API host
8
+ HOST=localhost
9
+ # API port
10
+ PORT=3333
11
+ # Database
12
+ DB_HOST=127.0.0.1
13
+ DB_PORT=3306
14
+ DB_USER=root
15
+ DB_PASSWORD=
16
+ DB_NAME=your_database_name
17
+ # Session driver
18
+ SESSION_DRIVER=cookie
19
+ # Client app url
20
+ CLIENT_APP_URL=http://localhost:5173
21
+ # JWT
22
+ JWT_SECRET=your_very_long_random_secret_key_here
23
+ JWT_SECRET_REFRESH=your_different_very_long_refresh_secret_here
24
+ JWT_ALGORITHM=HS256
25
+ # Mailer
26
+ MAILER_HOST=mailer_dns
27
+ MAILER_PORT=465
28
+ MAILER_USER=mailer_user
29
+ MAILER_PASS=mailer_password
30
+ MAILER_SENDER=donotrespond@mail.com
@@ -0,0 +1,51 @@
1
+ # Dependencies
2
+ node_modules/
3
+ package-lock.json
4
+
5
+ # Environment variables
6
+ .env
7
+ .env.local
8
+ .env.*.local
9
+
10
+ # Build output
11
+ dist/
12
+ build/
13
+ *.tsbuildinfo
14
+
15
+ # Logs
16
+ logs/
17
+ *.log
18
+ npm-debug.log*
19
+ yarn-debug.log*
20
+ yarn-error.log*
21
+ lerna-debug.log*
22
+
23
+ # Runtime data
24
+ pids/
25
+ *.pid
26
+ *.seed
27
+ *.pid.lock
28
+
29
+ # Coverage directory
30
+ coverage/
31
+ .nyc_output/
32
+
33
+ # IDE and editors
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
38
+ *~
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Optional npm cache directory
43
+ .npm
44
+
45
+ # Optional eslint cache
46
+ .eslintcache
47
+
48
+ # Temporary files
49
+ tmp/
50
+ temp/
51
+ *.tmp
@@ -0,0 +1,2 @@
1
+ build
2
+ node_modules
@@ -0,0 +1,8 @@
1
+ {
2
+ "semi": false,
3
+ "tabWidth": 2,
4
+ "singleQuote": false,
5
+ "trailingComma": "none",
6
+ "printWidth": 120,
7
+ "endOfLine": "lf"
8
+ }
@@ -0,0 +1,39 @@
1
+ # LyraJS
2
+
3
+ A simple framework for building web API with Node.js.
4
+
5
+ We wanted to create a framework as similar as possible to the Symfony framework, with a CLI to help you create your API faster.
6
+
7
+ By default, the framework provides :
8
+ - Routing
9
+ - Middlewares
10
+ - User and role entities, repositories, controllers and routes
11
+ - Authentication with JWT
12
+ - CORS
13
+ - ORM for MySQL database managment
14
+ - Error handling
15
+ - CLI to help you create your API faster
16
+
17
+ Our CLI includes :
18
+ - A command to create a new entity and repository
19
+ - A command to create a new controller
20
+ - A command to create a new migration
21
+ - A command to apply last migration to make database tables corresponding to entities
22
+ - A command to load fixtures data in database
23
+ - A command "help" to display all available commands
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ git clone https://github.com/devway-eu/LyraJS.git
29
+ ```
30
+
31
+ ```bash
32
+ npm install
33
+ ```
34
+
35
+ ## Run services
36
+
37
+ ```bash
38
+ npm run dev
39
+ ```
@@ -0,0 +1,6 @@
1
+ database:
2
+ host: "%env(DB_HOST)%"
3
+ port: "%env(DB_PORT)%"
4
+ user: "%env(DB_USER)%"
5
+ password: "%env(DB_PASSWORD)%"
6
+ name: "%env(DB_NAME)%"
@@ -0,0 +1,6 @@
1
+ mailer:
2
+ host: "%env(MAILER_HOST)%"
3
+ port: "%env(MAILER_PORT)%"
4
+ username: "%env(MAILER_USERNAME)%"
5
+ password: "%env(MAILER_PASSWORD)%"
6
+ sender: "%env(MAILER_SENDER)%"
@@ -0,0 +1,7 @@
1
+ parameters:
2
+ api_name: "LyraJS API"
3
+ api_version: "1.0.0"
4
+ api_host: "%env(HOST)%"
5
+ api_port: "%env(PORT)%"
6
+ api_env: "%env(NODE_ENV)%"
7
+ api_timezone: "%env(TZ)%"
@@ -0,0 +1,2 @@
1
+ router:
2
+ base_path: /api
@@ -0,0 +1,22 @@
1
+ security:
2
+ jwt:
3
+ secret_key: "%env(JWT_SECRET)%"
4
+ token_expiration: 3600
5
+ secret_key_refresh: "%env(JWT_SECRET_REFRESH)%"
6
+ refresh_token_expiration: 86400
7
+ algorithm: "%env(JWT_ALGORITHM)%"
8
+ auth_routes:
9
+ sign_out: "/api/v1/sign-out"
10
+ limits:
11
+ request_max_size: "%env(REQUEST_MAX_SIZE)%"
12
+ rate_limiter:
13
+ time: 900000 # 15 minutes (ms)
14
+ max_attempts: 5
15
+ message: 'Too many login attempts, please try again later'
16
+ access_control:
17
+ - { path: /api/v1/auth/sign-out, roles: [ROLE_USER] }
18
+ - { path: /api/v1/auth/user, roles: [ROLE_USER] }
19
+ - { path: /api/v1/admin, roles: [ROLE_ADMIN] }
20
+ role_hierarchy:
21
+ ROLE_ADMIN: ROLE_USER
22
+ ROLE_SUPER_ADMIN: ROLE_ADMIN
@@ -0,0 +1,53 @@
1
+ import js from "@eslint/js"
2
+ import globals from "globals"
3
+ import tseslint from "typescript-eslint"
4
+ import eslintConfigPrettier from "eslint-config-prettier"
5
+ import eslintPluginPrettier from "eslint-plugin-prettier"
6
+ import eslintPluginSimpleImportSort from "eslint-plugin-simple-import-sort"
7
+
8
+ export default tseslint.config(
9
+ { ignores: ["dist"] },
10
+ {
11
+ extends: [js.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier],
12
+ files: ["**/*.{ts,tsx}"],
13
+ languageOptions: {
14
+ ecmaVersion: 2020,
15
+ globals: globals.node
16
+ },
17
+ plugins: {
18
+ prettier: eslintPluginPrettier,
19
+ "simple-import-sort": eslintPluginSimpleImportSort
20
+ },
21
+ rules: {
22
+ "no-useless-catch": "off",
23
+ "prettier/prettier": "warn",
24
+ "@typescript-eslint/no-unused-vars": [
25
+ "error",
26
+ {
27
+ argsIgnorePattern: "^_",
28
+ varsIgnorePattern: "^_",
29
+ caughtErrorsIgnorePattern: "^_"
30
+ }
31
+ ],
32
+ "@typescript-eslint/no-throw-literal": "off",
33
+ "no-throw-literal": "off",
34
+ "@typescript-eslint/only-throw-error": "off",
35
+ "@typescript-eslint/prefer-promise-reject-errors": "off",
36
+ "simple-import-sort/imports": [
37
+ "error",
38
+ {
39
+ groups: [
40
+ ["^\\u0000"],
41
+ ["^@?\\w"],
42
+ [
43
+ "^(@app|@config|@core|@controllers|@fixtures|@middlewares|@entity|@repository|@router|@services|@tests|@types)(/.*|$)"
44
+ ],
45
+ ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
46
+ ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
47
+ ["^.+\\.s?css$"]
48
+ ]
49
+ }
50
+ ]
51
+ }
52
+ }
53
+ )
@@ -0,0 +1,3 @@
1
+ USE `nodejs_sql`;
2
+ DROP TABLE IF EXISTS `user`;
3
+ CREATE TABLE IF NOT EXISTS `user` (`id` BIGINT AUTO_INCREMENT NOT NULL, `username` VARCHAR (255) NOT NULL , `firstname` VARCHAR (255) NOT NULL , `lastname` VARCHAR (255) NOT NULL , `email` VARCHAR (255) NOT NULL UNIQUE, `password` VARCHAR (255) NOT NULL , `role` VARCHAR (255) NOT NULL , `refresh_token` VARCHAR (255) , `created_at` TIMESTAMP NOT NULL , `updated_at` TIMESTAMP NOT NULL , PRIMARY KEY (`id`));
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "LyraJS API",
3
+ "type": "module",
4
+ "scripts": {
5
+ "dev": "tsx watch src/server.ts",
6
+ "start": "tsx src/server.ts",
7
+ "maestro": "maestro"
8
+ },
9
+ "dependencies": {
10
+ "bcrypt": "^6.0.0",
11
+ "cookie-parser": "^1.4.7",
12
+ "cors": "^2.8.5",
13
+ "dotenv": "^16.5.0",
14
+ "express": "^5.1.0",
15
+ "express-rate-limit": "^8.2.1",
16
+ "jsonwebtoken": "^9.0.2",
17
+ "mysql2": "^3.14.1",
18
+ "nodemailer": "^7.0.7",
19
+ "reflect-metadata": "^0.2.2",
20
+ "yaml": "^2.8.0"
21
+ },
22
+ "devDependencies": {
23
+ "@lyrajs/core": "^1.0.0",
24
+ "@types/bcrypt": "^5.0.2",
25
+ "@types/cookie-parser": "^1.4.8",
26
+ "@types/cors": "^2.8.18",
27
+ "@types/dotenv": "^8.2.3",
28
+ "@types/express": "^5.0.1",
29
+ "@types/jsonwebtoken": "^9.0.9",
30
+ "@types/node": "^22.15.17",
31
+ "@types/nodemailer": "^6.4.17",
32
+ "@typescript-eslint/eslint-plugin": "^8.23.0",
33
+ "@typescript-eslint/parser": "^8.23.0",
34
+ "eslint": "^9.20.0",
35
+ "eslint-config-prettier": "^10.0.1",
36
+ "eslint-plugin-prettier": "^5.2.3",
37
+ "eslint-plugin-simple-import-sort": "^12.1.1",
38
+ "inquirer": "^12.7.0",
39
+ "tsx": "^4.19.4",
40
+ "typescript": "^5.7.3",
41
+ "typescript-eslint": "^8.23.0"
42
+ }
43
+ }
@@ -0,0 +1,124 @@
1
+ import bcrypt from "bcrypt"
2
+ import { NextFunction, Request, Response } from "express"
3
+ import jwt from "jsonwebtoken"
4
+
5
+ import { SecurityConfig } from "@lyrajs/core"
6
+ import { AuthenticatedRequest, UnauthorizedException, Validator } from "@lyrajs/core"
7
+ import { User } from "@entity/User"
8
+ import { userRepository } from "@repository/UserRepository"
9
+
10
+ const securityConfig = new SecurityConfig().getConfig()
11
+
12
+ export class AuthController {
13
+ static signUp = async (req: Request, res: Response, next: NextFunction) => {
14
+ try {
15
+ const { username, firstname, lastname, email, password } = req.body
16
+
17
+ if (!username || !firstname || !lastname || !email || !password) {
18
+ throw new Error("Missing required fields")
19
+ }
20
+
21
+ if (!Validator.isUsernameValid(username)) {
22
+ throw new Error("Invalid username")
23
+ }
24
+
25
+ if (!Validator.isEmailValid(email)) {
26
+ throw new Error("Invalid email")
27
+ }
28
+
29
+ const isEmailUsed = await userRepository.findOneBy({ email })
30
+
31
+ if (isEmailUsed) {
32
+ throw new Error("Email already in use")
33
+ }
34
+
35
+ if (!Validator.isPasswordValid(password)) {
36
+ throw new Error("Invalid password")
37
+ }
38
+
39
+ const user = new User()
40
+ const hashedPassword = await bcrypt.hash(password, 10)
41
+
42
+ user.username = username
43
+ user.firstname = firstname
44
+ user.lastname = lastname
45
+ user.email = email
46
+ user.password = hashedPassword
47
+ user.role = 'ROLE_USER'
48
+
49
+ await userRepository.save(user)
50
+
51
+ const registeredUser = await userRepository.findOneBy({ email })
52
+
53
+ delete registeredUser?.password
54
+
55
+ res.status(201).json({ message: "User registered successfully", user: registeredUser })
56
+ } catch (error) {
57
+ next(error)
58
+ }
59
+ }
60
+
61
+ static signIn = async (req: Request, res: Response, next: NextFunction) => {
62
+ try {
63
+ const { email, password } = req.body
64
+
65
+ if (!email || !password) {
66
+ throw new Error("Missing required fields")
67
+ }
68
+
69
+ const user = await userRepository.findOneBy({ email })
70
+
71
+ if (!user || !(user && (await bcrypt.compare(password, user.password)))) {
72
+ throw new Error("Invalid credentials")
73
+ }
74
+
75
+ const token = jwt.sign({ id: user.id }, securityConfig.jwt.secret_key as string, {
76
+ algorithm: securityConfig.jwt.algorithm as string,
77
+ expiresIn: securityConfig.jwt.token_expiration
78
+ })
79
+
80
+ user.refresh_token = jwt.sign({ id: user.id }, securityConfig.jwt.secret_key_refresh as string, {
81
+ algorithm: securityConfig.jwt.algorithm as string,
82
+ expiresIn: securityConfig.jwt.refresh_token_expiration
83
+ })
84
+
85
+ await userRepository.save(user)
86
+
87
+ res.cookie("Token", token, {
88
+ sameSite: "Lax",
89
+ httpOnly: true,
90
+ secure: process.env.ENV === "production",
91
+ maxAge: 1000 * 60 * 60 * 24,
92
+ partitioned: false
93
+ })
94
+
95
+ delete user.password
96
+
97
+ res.status(200).json({ message: "User authenticated in successfully", user, token })
98
+ } catch (error) {
99
+ next(error)
100
+ }
101
+ }
102
+
103
+ static getAuthenticatedUser = async (req: AuthenticatedRequest<Request>, res: Response, next: NextFunction) => {
104
+ try {
105
+ const user = req.user as User
106
+
107
+ if (!user) throw new UnauthorizedException()
108
+
109
+ res.status(200).json({
110
+ firstname: user.firstname,
111
+ lastname: user.lastname,
112
+ email: user.email,
113
+ role: user.role
114
+ })
115
+ } catch (error) {
116
+ next(error)
117
+ }
118
+ }
119
+
120
+ static signOut = async (_req: Request, res: Response) => {
121
+ res.clearCookie("Token")
122
+ res.status(200).json({ message: "Unauthenticated successfully" })
123
+ }
124
+ }
@@ -0,0 +1,91 @@
1
+ import { NextFunction, Request, Response } from "express"
2
+ import { Validator, ValidationException } from "@lyrajs/core"
3
+ import { User } from "@entity/User"
4
+ import { userRepository } from "@repository/UserRepository"
5
+
6
+ export class UserController {
7
+ static list = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
8
+ try {
9
+ const users = await userRepository.findAll().map( (user: User) => {
10
+ delete user.password
11
+ return user
12
+ } )
13
+ res.status(200).json({ message: "Users fetched successfully", users })
14
+ } catch (error) {
15
+ next(error)
16
+ }
17
+ }
18
+
19
+ static read = async (req: Request, res: Response, next: NextFunction) => {
20
+ try {
21
+ const { id } = req.params
22
+ const user = await userRepository.find(id)
23
+ delete user?.password
24
+ res.status(200).json({ message: "User fetched successfully", user })
25
+ } catch (error) {
26
+ next(error)
27
+ }
28
+ }
29
+
30
+ static create = async (req: Request, res: Response, next: NextFunction) => {
31
+ try {
32
+ const { data }: {data: User} = req.body
33
+
34
+ if(
35
+ !data.email || !data.password ||
36
+ !data.firstname || !data.lastname ||
37
+ !data.username
38
+ )
39
+ {
40
+ new ValidationException("All fields are required.")
41
+ }
42
+
43
+ if( !Validator.isUsernameValid(data.username) )
44
+ {
45
+ new ValidationException("Username to short (min 2 characters, only alphabetical characters and underscores )")
46
+ }
47
+
48
+ if( !Validator.isEmailValid(data.email) )
49
+ {
50
+ new ValidationException("Invalid email format.")
51
+ }
52
+
53
+ if( !Validator.isPasswordValid(data.password) )
54
+ {
55
+ new ValidationException("Password is to weak. I must be 10 characters long, including at least 1 lowercase, 1 uppercase, 1 number and 1 special character.")
56
+ }
57
+
58
+ data.role = "ROLE_USER";
59
+
60
+ await userRepository.save(data)
61
+ res.status(201).json({ message: "User created successfully" })
62
+ } catch (error) {
63
+ next(error)
64
+ }
65
+ }
66
+
67
+ static update = async (req: Request, res: Response, next: NextFunction) => {
68
+ try {
69
+ const { data }: {data: User} = req.body
70
+ const user = await userRepository.find(data.id)
71
+ if (!user) res.status(404).json({ message: "User not found" })
72
+ if (user) await userRepository.save(data)
73
+ res.status(200).json({ message: "Users updated successfully" })
74
+ } catch (error) {
75
+ next(error)
76
+ }
77
+ }
78
+
79
+ static delete = async (req: Request, res: Response, next: NextFunction) => {
80
+ try {
81
+ const { id } = req.params
82
+ const user = await userRepository.find(id)
83
+ if (!user) res.status(404).json({ message: "User not found" })
84
+ if (!user?.id) res.status(400).json({ message: "Invalid user id" })
85
+ if (user?.id && id) await userRepository.delete(id)
86
+ res.status(200).json({ message: "User deleted successfully" })
87
+ } catch (error) {
88
+ next(error)
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,29 @@
1
+ import { Column, Entity, Table } from "@lyrajs/core"
2
+
3
+ @Table()
4
+ export class User extends Entity<User> {
5
+ @Column({ type: "bigint", pk: true })
6
+ id: number
7
+ @Column({ type: "varchar", size: 255 })
8
+ username: string = ""
9
+ @Column({ type: "varchar", size: 255 })
10
+ firstname: string = ""
11
+ @Column({ type: "varchar", size: 255 })
12
+ lastname: string = ""
13
+ @Column({ type: "varchar", size: 255, unique: true })
14
+ email: string = ""
15
+ @Column({ type: "varchar", size: 255 })
16
+ password: string = ""
17
+ @Column({ type: "varchar", size: 255 })
18
+ role: string = "ROLE_USER"
19
+ @Column({ type: "varchar", size: 255, nullable: true })
20
+ refresh_token: string | null = null
21
+ @Column({ type: "timestamp" })
22
+ created_at: string | Date | null = new Date()
23
+ @Column({ type: "timestamp" })
24
+ updated_at: string | Date | null = new Date()
25
+
26
+ constructor(user?: Partial<User> | User) {
27
+ super(user)
28
+ }
29
+ }
@@ -0,0 +1,49 @@
1
+ import bcrypt from "bcrypt"
2
+
3
+ import { User } from "@entity/User"
4
+ import { userRepository } from "@repository/UserRepository"
5
+
6
+ export class AppFixtures {
7
+ private users = [
8
+ {
9
+ username: "Mithrandil",
10
+ firstname: "Gandalf",
11
+ lastname: "Thewhite",
12
+ email: "gandalf@mail.com",
13
+ password: "Y0u$ha11n0tpa$$!"
14
+ },
15
+ {
16
+ username: "Pippin",
17
+ firstname: "Peregrin",
18
+ lastname: "Took",
19
+ email: "pippin@mail.com",
20
+ password: "Mu$hr0000m$!!"
21
+ },
22
+ {
23
+ username: "Merry",
24
+ firstname: "Meriadoc",
25
+ lastname: "Brandybuck",
26
+ email: "merry@mail.com",
27
+ password: "Mu$hr0000m$!!"
28
+ }
29
+ ]
30
+
31
+ load = async () => {
32
+ await this.loadUsers()
33
+ }
34
+
35
+ private loadUsers = async () => {
36
+ for (const u of this.users) {
37
+ const hashedPassword = await bcrypt.hash(u.password, 10)
38
+ const user = new User()
39
+ user.username = u.username
40
+ user.firstname = u.firstname
41
+ user.lastname = u.lastname
42
+ user.email = u.email
43
+ user.password = hashedPassword
44
+ user.created_at = new Date()
45
+ user.updated_at = new Date()
46
+ await userRepository.save(user)
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,6 @@
1
+ import { NextFunction, Request, Response } from "express"
2
+
3
+ export const YourMiddleware = (req: Request, res: Response, next: NextFunction) => {
4
+ console.log("Your middleware checks or does something here...")
5
+ next()
6
+ }
@@ -0,0 +1,10 @@
1
+ import { Repository } from "@lyrajs/core"
2
+ import { User } from "@entity/User"
3
+
4
+ class UserRepository extends Repository<User> {
5
+ constructor() {
6
+ super(User)
7
+ }
8
+ }
9
+
10
+ export const userRepository = new UserRepository()
@@ -0,0 +1,14 @@
1
+ import { Response, Router } from "express"
2
+
3
+ import { Config } from "@lyrajs/core"
4
+ import { routes } from "@router/routes"
5
+
6
+ const routerBasePath = new Config().get("router.base_path")
7
+
8
+ export const router = Router()
9
+
10
+ router.use(routerBasePath, routes)
11
+
12
+ router.get("/", (req, res: Response) => {
13
+ res.send("Nothing here...")
14
+ })
@@ -0,0 +1,10 @@
1
+ import { AuthController } from "@controller/AuthController"
2
+ import { Router } from "express"
3
+ import { rateLimiter } from "@lyrajs/core"
4
+
5
+ export const authRoutes = Router()
6
+
7
+ authRoutes.post("/sign-up", AuthController.signUp)
8
+ authRoutes.post("/sign-in", rateLimiter, AuthController.signIn)
9
+ authRoutes.get("/user", AuthController.getAuthenticatedUser)
10
+ authRoutes.get("/sign-out", AuthController.signOut)
@@ -0,0 +1,9 @@
1
+ import { Router } from "express"
2
+
3
+ import { authRoutes } from "./authRoutes"
4
+ import { userRoutes } from "./userRoutes"
5
+
6
+ export const routes = Router()
7
+
8
+ routes.use("/auth", authRoutes)
9
+ routes.use("/user", userRoutes)
@@ -0,0 +1,10 @@
1
+ import { UserController } from "@controller/UserController"
2
+ import { Router } from "express"
3
+
4
+ export const userRoutes = Router()
5
+
6
+ userRoutes.get("/all", UserController.list)
7
+ userRoutes.get("/:id", UserController.read)
8
+ userRoutes.put("/", UserController.create)
9
+ userRoutes.patch("/:id", UserController.update)
10
+ userRoutes.delete("/:id", UserController.delete)
@@ -0,0 +1,44 @@
1
+ import cookieParser from "cookie-parser"
2
+ import cors from "cors"
3
+ import * as dotenv from "dotenv"
4
+ import express from "express"
5
+ import * as process from "node:process"
6
+
7
+ import { router } from "@app/router"
8
+ import { Config, LyraConsole, accessMiddleware, errorHandler, httpRequestMiddleware } from "@lyrajs/core"
9
+
10
+ dotenv.config()
11
+
12
+ const params = new Config().get("parameters")
13
+ const securityConfig = new Config().get("security")
14
+
15
+ const port = process.env.PORT
16
+ const app = express()
17
+
18
+ app.set("trust proxy", true)
19
+ app.use(cookieParser())
20
+ app.use(express.json({ limit: securityConfig.limits.request_max_size || "10mb" }))
21
+ app.use(express.urlencoded({ limit: securityConfig.limits.request_max_size || "10mb", extended: true }))
22
+ app.use(
23
+ cors({
24
+ origin: `${process.env.CLIENT_APP_URL}`,
25
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
26
+ credentials: true
27
+ })
28
+ )
29
+ app.use(httpRequestMiddleware)
30
+ app.use(accessMiddleware)
31
+ app.use(router)
32
+ app.use(errorHandler)
33
+
34
+ app.listen(port, (err?: Error) => {
35
+ if (err) {
36
+ LyraConsole.error("Error starting server:", err.message)
37
+ } else {
38
+ LyraConsole.info(
39
+ `${params.api_name} v${params.api_version}`,
40
+ `Server running at ${params.api_host}:${params.api_port}`,
41
+ ""
42
+ )
43
+ }
44
+ })
@@ -0,0 +1,5 @@
1
+ export class YourService {
2
+ exemple() {
3
+ console.log("Here is a method of your service...")
4
+ }
5
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+
3
+ function exemple(arg) {
4
+ return arg
5
+ }
6
+
7
+ // i.e. using Jest :
8
+
9
+ expect(exemple("your value")).toBe("your value")
10
+
11
+ */
@@ -0,0 +1,4 @@
1
+ export interface ExempleType {
2
+ id: number
3
+ name: string
4
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "esnext",
4
+ "baseUrl": ".",
5
+ "target": "es2017",
6
+ "lib": [
7
+ "esnext"
8
+ ],
9
+ "moduleResolution": "node",
10
+ "esModuleInterop": true,
11
+ "strict": true,
12
+ "strictNullChecks": true,
13
+ "resolveJsonModule": true,
14
+ "skipDefaultLibCheck": true,
15
+ "emitDecoratorMetadata": true,
16
+ "experimentalDecorators": true,
17
+ "outDir": "./dist",
18
+ "strictPropertyInitialization": false,
19
+ "noUnusedLocals": false,
20
+ "noUnusedParameters": false,
21
+ "paths": {
22
+ "@app/*": [
23
+ "./src/*"
24
+ ],
25
+ "@config/*": [
26
+ "./config/*"
27
+ ],
28
+ "@controller/*": [
29
+ "./src/controller/*"
30
+ ],
31
+ "@fixtures/*": [
32
+ "./src/fixtures/*"
33
+ ],
34
+ "@middleware/*": [
35
+ "./src/middleware/*"
36
+ ],
37
+ "@entity/*": [
38
+ "./src/entity/*"
39
+ ],
40
+ "@repository/*": [
41
+ "./src/repository/*"
42
+ ],
43
+ "@router/*": [
44
+ "./src/router/*"
45
+ ],
46
+ "@services/*": [
47
+ "./src/services/*"
48
+ ],
49
+ "@tests/*": [
50
+ "./src/tests/*"
51
+ ],
52
+ "@types/*": [
53
+ "./src/types/*"
54
+ ]
55
+ }
56
+ },
57
+ "ts-node": {
58
+ "require": [
59
+ "tsconfig-paths/register"
60
+ ]
61
+ }
62
+ }