@forgedevstack/harbor 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/CHANGELOG.md +126 -0
- package/README.md +927 -0
- package/dist/changelog/index.d.ts +3 -0
- package/dist/changelog/index.d.ts.map +1 -0
- package/dist/changelog/manager.d.ts +29 -0
- package/dist/changelog/manager.d.ts.map +1 -0
- package/dist/changelog/types.d.ts +41 -0
- package/dist/changelog/types.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/constants/config.const.d.ts +11 -0
- package/dist/constants/config.const.d.ts.map +1 -0
- package/dist/constants/defaults.const.d.ts +10 -0
- package/dist/constants/defaults.const.d.ts.map +1 -0
- package/dist/constants/http.const.d.ts +43 -0
- package/dist/constants/http.const.d.ts.map +1 -0
- package/dist/constants/index.d.ts +5 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/validation.const.d.ts +35 -0
- package/dist/constants/validation.const.d.ts.map +1 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/errorHandler.d.ts +25 -0
- package/dist/core/errorHandler.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/router.d.ts +68 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/server.d.ts +4 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/database/connection.d.ts +39 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/index.d.ts +28 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/model.d.ts +118 -0
- package/dist/database/model.d.ts.map +1 -0
- package/dist/database/schema.d.ts +63 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/types.d.ts +270 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/docker/index.d.ts +3 -0
- package/dist/docker/index.d.ts.map +1 -0
- package/dist/docker/index.js +2 -0
- package/dist/docker/index.js.map +1 -0
- package/dist/docker/manager.d.ts +21 -0
- package/dist/docker/manager.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +38 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/locales/en.d.ts +2 -0
- package/dist/i18n/locales/en.d.ts.map +1 -0
- package/dist/i18n/locales/he.d.ts +2 -0
- package/dist/i18n/locales/he.d.ts.map +1 -0
- package/dist/index.cjs.js +24 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +2094 -0
- package/dist/index.es.js.map +1 -0
- package/dist/logger-D7aJSi62.mjs +102 -0
- package/dist/logger-D7aJSi62.mjs.map +1 -0
- package/dist/logger-DEnWXtpk.js +3 -0
- package/dist/logger-DEnWXtpk.js.map +1 -0
- package/dist/manager-B1UKMjXW.js +4 -0
- package/dist/manager-B1UKMjXW.js.map +1 -0
- package/dist/manager-B6vqJgEn.mjs +152 -0
- package/dist/manager-B6vqJgEn.mjs.map +1 -0
- package/dist/portal.d.ts +13 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/types/config.types.d.ts +83 -0
- package/dist/types/config.types.d.ts.map +1 -0
- package/dist/types/docker.types.d.ts +92 -0
- package/dist/types/docker.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logger.types.d.ts +35 -0
- package/dist/types/logger.types.d.ts.map +1 -0
- package/dist/types/route.types.d.ts +78 -0
- package/dist/types/route.types.d.ts.map +1 -0
- package/dist/types/server.types.d.ts +67 -0
- package/dist/types/server.types.d.ts.map +1 -0
- package/dist/types/validation.types.d.ts +64 -0
- package/dist/types/validation.types.d.ts.map +1 -0
- package/dist/utils/helpers.d.ts +7 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/httpLogger.d.ts +52 -0
- package/dist/utils/httpLogger.d.ts.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/object.d.ts +6 -0
- package/dist/utils/object.d.ts.map +1 -0
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/mongo.d.ts +13 -0
- package/dist/validation/mongo.d.ts.map +1 -0
- package/dist/validation/paramValidators.d.ts +18 -0
- package/dist/validation/paramValidators.d.ts.map +1 -0
- package/dist/validation/validators.d.ts +9 -0
- package/dist/validation/validators.d.ts.map +1 -0
- package/harbor.config.example.json +72 -0
- package/package.json +107 -0
- package/templates/default/.eslintrc.json +45 -0
- package/templates/default/README.md +97 -0
- package/templates/default/constants/config.ts +26 -0
- package/templates/default/constants/http.ts +32 -0
- package/templates/default/constants/index.ts +3 -0
- package/templates/default/controllers/index.ts +6 -0
- package/templates/default/controllers/user.controller.ts +77 -0
- package/templates/default/env.example +22 -0
- package/templates/default/harbor.version.json +7 -0
- package/templates/default/models/index.ts +6 -0
- package/templates/default/models/user.model.ts +68 -0
- package/templates/default/package.json +44 -0
- package/templates/default/routes/index.ts +12 -0
- package/templates/default/routes/user.routes.ts +21 -0
- package/templates/default/server.ts +45 -0
- package/templates/default/services/index.ts +6 -0
- package/templates/default/services/user.service.ts +84 -0
- package/templates/default/tsconfig.json +35 -0
- package/templates/default/types/index.ts +57 -0
- package/templates/default/utils/asyncHandler.ts +14 -0
- package/templates/default/utils/index.ts +5 -0
- package/templates/default/utils/logger.ts +52 -0
- package/templates/default/utils/response.ts +24 -0
- package/templates/default/utils/validation.ts +23 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
{{PROJECT_DESCRIPTION}}
|
|
4
|
+
|
|
5
|
+
Built with [Harbor](https://github.com/yaghobieh/Harbor) - The pipeline for Node.js backends.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🚀 **Express Server** - Fast and minimal web framework
|
|
10
|
+
- 📦 **MongoDB** - Database with Harbor ODM
|
|
11
|
+
- 🔒 **TypeScript** - Type-safe development
|
|
12
|
+
- 📝 **ESLint** - Code linting and formatting
|
|
13
|
+
- 🧪 **Vitest** - Testing framework
|
|
14
|
+
- 📁 **Clean Architecture** - Organized folder structure
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
### Prerequisites
|
|
19
|
+
|
|
20
|
+
- Node.js 18+
|
|
21
|
+
- MongoDB (local or Atlas)
|
|
22
|
+
|
|
23
|
+
### Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Install dependencies
|
|
27
|
+
npm install
|
|
28
|
+
|
|
29
|
+
# Copy environment variables
|
|
30
|
+
cp .env.example .env
|
|
31
|
+
|
|
32
|
+
# Start development server
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Available Scripts
|
|
37
|
+
|
|
38
|
+
| Script | Description |
|
|
39
|
+
|--------|-------------|
|
|
40
|
+
| `npm run dev` | Start development server with hot reload |
|
|
41
|
+
| `npm run build` | Build for production |
|
|
42
|
+
| `npm start` | Start production server |
|
|
43
|
+
| `npm run lint` | Run ESLint |
|
|
44
|
+
| `npm run lint:fix` | Fix ESLint errors |
|
|
45
|
+
| `npm test` | Run tests |
|
|
46
|
+
|
|
47
|
+
## Project Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
├── server.ts # Application entry point
|
|
51
|
+
├── routes/ # Route definitions
|
|
52
|
+
│ ├── index.ts # Route aggregator
|
|
53
|
+
│ └── user.routes.ts # User routes
|
|
54
|
+
├── controllers/ # Request handlers
|
|
55
|
+
│ └── user.controller.ts
|
|
56
|
+
├── services/ # Business logic
|
|
57
|
+
│ └── user.service.ts
|
|
58
|
+
├── models/ # Database models
|
|
59
|
+
│ └── user.model.ts
|
|
60
|
+
├── types/ # TypeScript definitions
|
|
61
|
+
├── utils/ # Utility functions
|
|
62
|
+
├── constants/ # App constants & config
|
|
63
|
+
└── package.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API Endpoints
|
|
67
|
+
|
|
68
|
+
### Users
|
|
69
|
+
|
|
70
|
+
| Method | Endpoint | Description |
|
|
71
|
+
|--------|----------|-------------|
|
|
72
|
+
| GET | `/api/users` | Get all users |
|
|
73
|
+
| GET | `/api/users/:id` | Get user by ID |
|
|
74
|
+
| POST | `/api/users` | Create new user |
|
|
75
|
+
| PUT | `/api/users/:id` | Update user |
|
|
76
|
+
| DELETE | `/api/users/:id` | Delete user |
|
|
77
|
+
|
|
78
|
+
### Health Check
|
|
79
|
+
|
|
80
|
+
| Method | Endpoint | Description |
|
|
81
|
+
|--------|----------|-------------|
|
|
82
|
+
| GET | `/health` | Server health status |
|
|
83
|
+
|
|
84
|
+
## Environment Variables
|
|
85
|
+
|
|
86
|
+
| Variable | Description | Default |
|
|
87
|
+
|----------|-------------|---------|
|
|
88
|
+
| `PORT` | Server port | `3000` |
|
|
89
|
+
| `NODE_ENV` | Environment | `development` |
|
|
90
|
+
| `MONGODB_URI` | MongoDB connection string | - |
|
|
91
|
+
| `DB_NAME` | Database name | `harbor_app` |
|
|
92
|
+
| `JWT_SECRET` | JWT signing secret | - |
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
97
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
|
|
3
|
+
export const config = {
|
|
4
|
+
// Server
|
|
5
|
+
PORT: parseInt(process.env.PORT || '3000', 10),
|
|
6
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
7
|
+
|
|
8
|
+
// Database
|
|
9
|
+
MONGODB_URI: process.env.MONGODB_URI || '',
|
|
10
|
+
DB_NAME: process.env.DB_NAME || 'harbor_app',
|
|
11
|
+
|
|
12
|
+
// JWT
|
|
13
|
+
JWT_SECRET: process.env.JWT_SECRET || 'your-secret-key',
|
|
14
|
+
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '7d',
|
|
15
|
+
|
|
16
|
+
// CORS
|
|
17
|
+
CORS_ORIGIN: process.env.CORS_ORIGIN || '*',
|
|
18
|
+
|
|
19
|
+
// Logging
|
|
20
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
|
21
|
+
|
|
22
|
+
// Rate Limiting
|
|
23
|
+
RATE_LIMIT_WINDOW: parseInt(process.env.RATE_LIMIT_WINDOW || '900000', 10), // 15 minutes
|
|
24
|
+
RATE_LIMIT_MAX: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const HTTP_STATUS = {
|
|
2
|
+
OK: 200,
|
|
3
|
+
CREATED: 201,
|
|
4
|
+
NO_CONTENT: 204,
|
|
5
|
+
BAD_REQUEST: 400,
|
|
6
|
+
UNAUTHORIZED: 401,
|
|
7
|
+
FORBIDDEN: 403,
|
|
8
|
+
NOT_FOUND: 404,
|
|
9
|
+
CONFLICT: 409,
|
|
10
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
11
|
+
TOO_MANY_REQUESTS: 429,
|
|
12
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
13
|
+
BAD_GATEWAY: 502,
|
|
14
|
+
SERVICE_UNAVAILABLE: 503,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const HTTP_MESSAGES = {
|
|
18
|
+
[HTTP_STATUS.OK]: 'Success',
|
|
19
|
+
[HTTP_STATUS.CREATED]: 'Created successfully',
|
|
20
|
+
[HTTP_STATUS.NO_CONTENT]: 'Deleted successfully',
|
|
21
|
+
[HTTP_STATUS.BAD_REQUEST]: 'Bad request',
|
|
22
|
+
[HTTP_STATUS.UNAUTHORIZED]: 'Unauthorized',
|
|
23
|
+
[HTTP_STATUS.FORBIDDEN]: 'Access denied',
|
|
24
|
+
[HTTP_STATUS.NOT_FOUND]: 'Resource not found',
|
|
25
|
+
[HTTP_STATUS.CONFLICT]: 'Resource already exists',
|
|
26
|
+
[HTTP_STATUS.UNPROCESSABLE_ENTITY]: 'Validation error',
|
|
27
|
+
[HTTP_STATUS.TOO_MANY_REQUESTS]: 'Too many requests',
|
|
28
|
+
[HTTP_STATUS.INTERNAL_SERVER_ERROR]: 'Internal server error',
|
|
29
|
+
[HTTP_STATUS.BAD_GATEWAY]: 'Bad gateway',
|
|
30
|
+
[HTTP_STATUS.SERVICE_UNAVAILABLE]: 'Service unavailable',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { UserService } from '../services';
|
|
3
|
+
import { CreateUserDto, UpdateUserDto } from '../types';
|
|
4
|
+
|
|
5
|
+
export class UserController {
|
|
6
|
+
/**
|
|
7
|
+
* Get all users
|
|
8
|
+
*/
|
|
9
|
+
static async getAll(req: Request, res: Response) {
|
|
10
|
+
const { page = 1, limit = 10 } = req.query;
|
|
11
|
+
|
|
12
|
+
const result = await UserService.findAll({
|
|
13
|
+
page: Number(page),
|
|
14
|
+
limit: Number(limit),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return res.json(result);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get user by ID
|
|
22
|
+
*/
|
|
23
|
+
static async getById(req: Request, res: Response) {
|
|
24
|
+
const { id } = req.params;
|
|
25
|
+
|
|
26
|
+
const user = await UserService.findById(id);
|
|
27
|
+
|
|
28
|
+
if (!user) {
|
|
29
|
+
return res.status(404).json({ error: 'User not found' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return res.json(user);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create new user
|
|
37
|
+
*/
|
|
38
|
+
static async create(req: Request, res: Response) {
|
|
39
|
+
const data: CreateUserDto = req.body;
|
|
40
|
+
|
|
41
|
+
const user = await UserService.create(data);
|
|
42
|
+
|
|
43
|
+
return res.status(201).json(user);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Update user
|
|
48
|
+
*/
|
|
49
|
+
static async update(req: Request, res: Response) {
|
|
50
|
+
const { id } = req.params;
|
|
51
|
+
const data: UpdateUserDto = req.body;
|
|
52
|
+
|
|
53
|
+
const user = await UserService.update(id, data);
|
|
54
|
+
|
|
55
|
+
if (!user) {
|
|
56
|
+
return res.status(404).json({ error: 'User not found' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return res.json(user);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Delete user
|
|
64
|
+
*/
|
|
65
|
+
static async delete(req: Request, res: Response) {
|
|
66
|
+
const { id } = req.params;
|
|
67
|
+
|
|
68
|
+
const deleted = await UserService.delete(id);
|
|
69
|
+
|
|
70
|
+
if (!deleted) {
|
|
71
|
+
return res.status(404).json({ error: 'User not found' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return res.status(204).send();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Server Configuration
|
|
2
|
+
PORT=3000
|
|
3
|
+
NODE_ENV=development
|
|
4
|
+
|
|
5
|
+
# Database
|
|
6
|
+
MONGODB_URI=mongodb://localhost:27017
|
|
7
|
+
DB_NAME=harbor_app
|
|
8
|
+
|
|
9
|
+
# JWT Authentication
|
|
10
|
+
JWT_SECRET=your-super-secret-key-change-in-production
|
|
11
|
+
JWT_EXPIRES_IN=7d
|
|
12
|
+
|
|
13
|
+
# CORS
|
|
14
|
+
CORS_ORIGIN=http://localhost:3000
|
|
15
|
+
|
|
16
|
+
# Logging
|
|
17
|
+
LOG_LEVEL=info
|
|
18
|
+
|
|
19
|
+
# Rate Limiting
|
|
20
|
+
RATE_LIMIT_WINDOW=900000
|
|
21
|
+
RATE_LIMIT_MAX=100
|
|
22
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Schema, model } from 'harbor/database';
|
|
2
|
+
|
|
3
|
+
// Define the User schema
|
|
4
|
+
const userSchema = new Schema({
|
|
5
|
+
email: {
|
|
6
|
+
type: 'string',
|
|
7
|
+
required: true,
|
|
8
|
+
unique: true,
|
|
9
|
+
},
|
|
10
|
+
name: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
password: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
required: true,
|
|
17
|
+
select: false, // Don't include in query results by default
|
|
18
|
+
},
|
|
19
|
+
role: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['user', 'admin', 'moderator'],
|
|
22
|
+
default: 'user',
|
|
23
|
+
},
|
|
24
|
+
isActive: {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: true,
|
|
27
|
+
},
|
|
28
|
+
profile: {
|
|
29
|
+
avatar: { type: 'string' },
|
|
30
|
+
bio: { type: 'string' },
|
|
31
|
+
},
|
|
32
|
+
lastLogin: {
|
|
33
|
+
type: 'date',
|
|
34
|
+
},
|
|
35
|
+
}, {
|
|
36
|
+
timestamps: true, // Adds createdAt and updatedAt
|
|
37
|
+
collection: 'users',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Virtual for full display name
|
|
41
|
+
userSchema.virtual('displayName').get(function() {
|
|
42
|
+
return this.name || this.email.split('@')[0];
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Pre-save hook for password hashing
|
|
46
|
+
userSchema.pre('save', async function(next) {
|
|
47
|
+
if (this.isModified('password')) {
|
|
48
|
+
// Hash password here (use bcrypt in production)
|
|
49
|
+
// this.password = await bcrypt.hash(this.password, 10);
|
|
50
|
+
}
|
|
51
|
+
next();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Static method for finding by email
|
|
55
|
+
userSchema.statics.findByEmail = function(email: string) {
|
|
56
|
+
return this.findOne({ email: email.toLowerCase() });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Instance method for comparing passwords
|
|
60
|
+
userSchema.methods.comparePassword = async function(candidatePassword: string) {
|
|
61
|
+
// Compare passwords here (use bcrypt in production)
|
|
62
|
+
// return bcrypt.compare(candidatePassword, this.password);
|
|
63
|
+
return candidatePassword === this.password;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Export the model
|
|
67
|
+
export const User = model('User', userSchema);
|
|
68
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "{{PROJECT_DESCRIPTION}}",
|
|
5
|
+
"main": "dist/server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch server.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/server.js",
|
|
10
|
+
"lint": "eslint . --ext .ts",
|
|
11
|
+
"lint:fix": "eslint . --ext .ts --fix",
|
|
12
|
+
"test": "vitest",
|
|
13
|
+
"test:coverage": "vitest --coverage"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nodejs",
|
|
17
|
+
"express",
|
|
18
|
+
"mongodb",
|
|
19
|
+
"harbor",
|
|
20
|
+
"api"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"dotenv": "^16.3.1",
|
|
26
|
+
"express": "^4.18.2",
|
|
27
|
+
"harbor": "^1.2.0",
|
|
28
|
+
"mongodb": "^6.3.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/express": "^4.17.21",
|
|
32
|
+
"@types/node": "^20.10.0",
|
|
33
|
+
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
34
|
+
"@typescript-eslint/parser": "^6.13.0",
|
|
35
|
+
"eslint": "^8.55.0",
|
|
36
|
+
"tsx": "^4.6.0",
|
|
37
|
+
"typescript": "^5.3.0",
|
|
38
|
+
"vitest": "^1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { userRoutes } from './user.routes';
|
|
3
|
+
|
|
4
|
+
export const routes = Router();
|
|
5
|
+
|
|
6
|
+
// Register all route modules
|
|
7
|
+
routes.use('/users', userRoutes);
|
|
8
|
+
|
|
9
|
+
// Add more routes here:
|
|
10
|
+
// routes.use('/products', productRoutes);
|
|
11
|
+
// routes.use('/orders', orderRoutes);
|
|
12
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { GET, POST, PUT, DELETE } from 'harbor';
|
|
3
|
+
import { UserController } from '../controllers';
|
|
4
|
+
|
|
5
|
+
export const userRoutes = Router();
|
|
6
|
+
|
|
7
|
+
// GET /api/users - Get all users
|
|
8
|
+
userRoutes.get('/', GET(UserController.getAll));
|
|
9
|
+
|
|
10
|
+
// GET /api/users/:id - Get user by ID
|
|
11
|
+
userRoutes.get('/:id', GET(UserController.getById));
|
|
12
|
+
|
|
13
|
+
// POST /api/users - Create new user
|
|
14
|
+
userRoutes.post('/', POST(UserController.create));
|
|
15
|
+
|
|
16
|
+
// PUT /api/users/:id - Update user
|
|
17
|
+
userRoutes.put('/:id', PUT(UserController.update));
|
|
18
|
+
|
|
19
|
+
// DELETE /api/users/:id - Delete user
|
|
20
|
+
userRoutes.delete('/:id', DELETE(UserController.delete));
|
|
21
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createServer, connect, httpLogger } from 'harbor';
|
|
2
|
+
import { routes } from './routes';
|
|
3
|
+
import { config } from './constants';
|
|
4
|
+
|
|
5
|
+
async function bootstrap() {
|
|
6
|
+
// Create server with Harbor
|
|
7
|
+
const app = createServer({
|
|
8
|
+
port: config.PORT,
|
|
9
|
+
cors: true,
|
|
10
|
+
helmet: true,
|
|
11
|
+
json: true,
|
|
12
|
+
urlencoded: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// HTTP request logging
|
|
16
|
+
app.use(httpLogger({
|
|
17
|
+
format: 'dev',
|
|
18
|
+
skip: (req) => req.path === '/health',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Connect to MongoDB (optional)
|
|
22
|
+
if (config.MONGODB_URI) {
|
|
23
|
+
await connect(config.MONGODB_URI, {
|
|
24
|
+
dbName: config.DB_NAME,
|
|
25
|
+
});
|
|
26
|
+
console.log('📦 Connected to MongoDB');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Register all routes
|
|
30
|
+
app.use('/api', routes);
|
|
31
|
+
|
|
32
|
+
// Health check endpoint
|
|
33
|
+
app.get('/health', (req, res) => {
|
|
34
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Start server
|
|
38
|
+
app.listen(config.PORT, () => {
|
|
39
|
+
console.log(`🚀 Server running on http://localhost:${config.PORT}`);
|
|
40
|
+
console.log(`📚 API Docs: http://localhost:${config.PORT}/api-docs`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
bootstrap().catch(console.error);
|
|
45
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { User } from '../models';
|
|
2
|
+
import { CreateUserDto, UpdateUserDto, PaginationOptions, PaginatedResult } from '../types';
|
|
3
|
+
|
|
4
|
+
export class UserService {
|
|
5
|
+
/**
|
|
6
|
+
* Find all users with pagination
|
|
7
|
+
*/
|
|
8
|
+
static async findAll(options: PaginationOptions): Promise<PaginatedResult<typeof User>> {
|
|
9
|
+
const { page = 1, limit = 10 } = options;
|
|
10
|
+
const skip = (page - 1) * limit;
|
|
11
|
+
|
|
12
|
+
const [users, total] = await Promise.all([
|
|
13
|
+
User.find()
|
|
14
|
+
.skip(skip)
|
|
15
|
+
.limit(limit)
|
|
16
|
+
.sort({ createdAt: -1 }),
|
|
17
|
+
User.countDocuments(),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
data: users,
|
|
22
|
+
pagination: {
|
|
23
|
+
page,
|
|
24
|
+
limit,
|
|
25
|
+
total,
|
|
26
|
+
totalPages: Math.ceil(total / limit),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find user by ID
|
|
33
|
+
*/
|
|
34
|
+
static async findById(id: string) {
|
|
35
|
+
return User.findById(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find user by email
|
|
40
|
+
*/
|
|
41
|
+
static async findByEmail(email: string) {
|
|
42
|
+
return User.findOne({ email: email.toLowerCase() });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create new user
|
|
47
|
+
*/
|
|
48
|
+
static async create(data: CreateUserDto) {
|
|
49
|
+
const user = await User.create({
|
|
50
|
+
...data,
|
|
51
|
+
email: data.email.toLowerCase(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return user;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update user
|
|
59
|
+
*/
|
|
60
|
+
static async update(id: string, data: UpdateUserDto) {
|
|
61
|
+
return User.findByIdAndUpdate(
|
|
62
|
+
id,
|
|
63
|
+
{ $set: data },
|
|
64
|
+
{ new: true }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Delete user
|
|
70
|
+
*/
|
|
71
|
+
static async delete(id: string) {
|
|
72
|
+
const result = await User.deleteOne({ _id: id });
|
|
73
|
+
return result.deletedCount > 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if email exists
|
|
78
|
+
*/
|
|
79
|
+
static async emailExists(email: string): Promise<boolean> {
|
|
80
|
+
const count = await User.countDocuments({ email: email.toLowerCase() });
|
|
81
|
+
return count > 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"noImplicitAny": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"baseUrl": ".",
|
|
23
|
+
"paths": {
|
|
24
|
+
"@/*": ["./*"]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"include": [
|
|
28
|
+
"**/*.ts"
|
|
29
|
+
],
|
|
30
|
+
"exclude": [
|
|
31
|
+
"node_modules",
|
|
32
|
+
"dist"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// User DTOs
|
|
2
|
+
export interface CreateUserDto {
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
password: string;
|
|
6
|
+
role?: 'user' | 'admin' | 'moderator';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UpdateUserDto {
|
|
10
|
+
email?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
role?: 'user' | 'admin' | 'moderator';
|
|
14
|
+
isActive?: boolean;
|
|
15
|
+
profile?: {
|
|
16
|
+
avatar?: string;
|
|
17
|
+
bio?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Pagination
|
|
22
|
+
export interface PaginationOptions {
|
|
23
|
+
page?: number;
|
|
24
|
+
limit?: number;
|
|
25
|
+
sort?: string;
|
|
26
|
+
order?: 'asc' | 'desc';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Pagination {
|
|
30
|
+
page: number;
|
|
31
|
+
limit: number;
|
|
32
|
+
total: number;
|
|
33
|
+
totalPages: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PaginatedResult<T> {
|
|
37
|
+
data: T[];
|
|
38
|
+
pagination: Pagination;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// API Response
|
|
42
|
+
export interface ApiResponse<T = unknown> {
|
|
43
|
+
success: boolean;
|
|
44
|
+
data?: T;
|
|
45
|
+
error?: string;
|
|
46
|
+
message?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Request with user (for auth)
|
|
50
|
+
export interface AuthenticatedRequest extends Express.Request {
|
|
51
|
+
user?: {
|
|
52
|
+
id: string;
|
|
53
|
+
email: string;
|
|
54
|
+
role: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps async route handlers to automatically catch errors
|
|
5
|
+
* and pass them to Express error handling middleware
|
|
6
|
+
*/
|
|
7
|
+
export const asyncHandler = (
|
|
8
|
+
fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
|
|
9
|
+
): RequestHandler => {
|
|
10
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
11
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|