boiling-fullstack 0.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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +313 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/scaffolder.d.ts +2 -0
- package/dist/scaffolder.js +89 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +2 -0
- package/dist/utils/shell.d.ts +6 -0
- package/dist/utils/shell.js +53 -0
- package/dist/utils/template.d.ts +2 -0
- package/dist/utils/template.js +51 -0
- package/dist/utils/validation.d.ts +6 -0
- package/dist/utils/validation.js +59 -0
- package/package.json +40 -0
- package/templates/backend/nestjs/.dockerignore +3 -0
- package/templates/backend/nestjs/.eslintrc.cjs.ejs +19 -0
- package/templates/backend/nestjs/.prettierrc +6 -0
- package/templates/backend/nestjs/Dockerfile +44 -0
- package/templates/backend/nestjs/nest-cli.json +8 -0
- package/templates/backend/nestjs/package.json.ejs +52 -0
- package/templates/backend/nestjs/src/app.controller.ts +17 -0
- package/templates/backend/nestjs/src/app.module.ts.ejs +20 -0
- package/templates/backend/nestjs/src/app.service.ts +4 -0
- package/templates/backend/nestjs/src/auth/auth.controller.ts +19 -0
- package/templates/backend/nestjs/src/auth/auth.guard.ts +5 -0
- package/templates/backend/nestjs/src/auth/auth.module.ts +28 -0
- package/templates/backend/nestjs/src/auth/auth.service.ts +50 -0
- package/templates/backend/nestjs/src/auth/dto/login.dto.ts +10 -0
- package/templates/backend/nestjs/src/auth/dto/register.dto.ts +10 -0
- package/templates/backend/nestjs/src/auth/entities/user.entity.ts +25 -0
- package/templates/backend/nestjs/src/auth/jwt.strategy.ts +19 -0
- package/templates/backend/nestjs/src/config/data-source.ts +9 -0
- package/templates/backend/nestjs/src/config/typeorm.config.ts.ejs +8 -0
- package/templates/backend/nestjs/src/main.ts.ejs +20 -0
- package/templates/backend/nestjs/src/migrations/.gitkeep +0 -0
- package/templates/backend/nestjs/tsconfig.build.json +4 -0
- package/templates/backend/nestjs/tsconfig.json +21 -0
- package/templates/frontend/nuxt/.dockerignore +5 -0
- package/templates/frontend/nuxt/.eslintrc.cjs.ejs +5 -0
- package/templates/frontend/nuxt/.prettierrc +6 -0
- package/templates/frontend/nuxt/Dockerfile +38 -0
- package/templates/frontend/nuxt/app/app.vue +3 -0
- package/templates/frontend/nuxt/app/pages/index.vue.ejs +25 -0
- package/templates/frontend/nuxt/nuxt.config.ts.ejs +19 -0
- package/templates/frontend/nuxt/package.json.ejs +25 -0
- package/templates/frontend/nuxt/tsconfig.json +3 -0
- package/templates/frontend/vue/.dockerignore +3 -0
- package/templates/frontend/vue/.eslintrc.cjs.ejs +14 -0
- package/templates/frontend/vue/.prettierrc +6 -0
- package/templates/frontend/vue/Dockerfile +33 -0
- package/templates/frontend/vue/index.html.ejs +13 -0
- package/templates/frontend/vue/package.json.ejs +28 -0
- package/templates/frontend/vue/src/App.vue.ejs +22 -0
- package/templates/frontend/vue/src/main.ts +4 -0
- package/templates/frontend/vue/src/vite-env.d.ts +1 -0
- package/templates/frontend/vue/tsconfig.json +20 -0
- package/templates/frontend/vue/vite.config.ts.ejs +14 -0
- package/templates/root/.env.ejs +24 -0
- package/templates/root/.env.example.ejs +24 -0
- package/templates/root/.env.production.ejs +17 -0
- package/templates/root/Makefile.ejs +116 -0
- package/templates/root/README.md.ejs +158 -0
- package/templates/root/docker-compose.prod.yml.ejs +45 -0
- package/templates/root/docker-compose.yml.ejs +77 -0
- package/templates/root/gitignore +8 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setVerbose = setVerbose;
|
|
7
|
+
exports.runCommand = runCommand;
|
|
8
|
+
exports.checkCommand = checkCommand;
|
|
9
|
+
exports.checkEnvironment = checkEnvironment;
|
|
10
|
+
exports.gitInit = gitInit;
|
|
11
|
+
exports.npmInstall = npmInstall;
|
|
12
|
+
const execa_1 = __importDefault(require("execa"));
|
|
13
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
14
|
+
let verbose = false;
|
|
15
|
+
function setVerbose(v) {
|
|
16
|
+
verbose = v;
|
|
17
|
+
}
|
|
18
|
+
async function runCommand(command, args, cwd) {
|
|
19
|
+
try {
|
|
20
|
+
await (0, execa_1.default)(command, args, { cwd, stdio: verbose ? 'inherit' : 'ignore' });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT') {
|
|
24
|
+
throw new Error(`Command "${command}" not found. Make sure it is installed and available in your PATH.`);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Command "${command} ${args.join(' ')}" failed: ${error.shortMessage || error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function checkCommand(cmd) {
|
|
30
|
+
try {
|
|
31
|
+
await (0, execa_1.default)(cmd, ['--version'], { stdio: 'ignore' });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function checkEnvironment() {
|
|
39
|
+
const hasGit = await checkCommand('git');
|
|
40
|
+
if (!hasGit) {
|
|
41
|
+
throw new Error('git is required but was not found. Please install git before continuing.');
|
|
42
|
+
}
|
|
43
|
+
const hasDocker = await checkCommand('docker');
|
|
44
|
+
if (!hasDocker) {
|
|
45
|
+
console.warn(chalk_1.default.yellow('⚠ docker not found. The project will be generated but "make up" requires Docker.'));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function gitInit(cwd) {
|
|
49
|
+
await runCommand('git', ['init'], cwd);
|
|
50
|
+
}
|
|
51
|
+
async function npmInstall(cwd) {
|
|
52
|
+
await runCommand('npm', ['install'], cwd);
|
|
53
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderTemplateFile = renderTemplateFile;
|
|
7
|
+
exports.copyAndRenderDir = copyAndRenderDir;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const ejs_1 = __importDefault(require("ejs"));
|
|
11
|
+
async function renderTemplateFile(templatePath, outputPath, data) {
|
|
12
|
+
const template = await fs_extra_1.default.readFile(templatePath, 'utf-8');
|
|
13
|
+
const rendered = ejs_1.default.render(template, data, { filename: templatePath });
|
|
14
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
|
|
15
|
+
await fs_extra_1.default.writeFile(outputPath, rendered);
|
|
16
|
+
}
|
|
17
|
+
async function copyAndRenderDir(templateDir, outputDir, data) {
|
|
18
|
+
const files = await getAllFiles(templateDir);
|
|
19
|
+
for (const filePath of files) {
|
|
20
|
+
const relativePath = path_1.default.relative(templateDir, filePath);
|
|
21
|
+
let outputName = relativePath;
|
|
22
|
+
const isEjs = outputName.endsWith('.ejs');
|
|
23
|
+
if (isEjs) {
|
|
24
|
+
outputName = outputName.slice(0, -4);
|
|
25
|
+
}
|
|
26
|
+
// Rename gitignore → .gitignore (npm strips dotfiles on publish)
|
|
27
|
+
outputName = outputName.replace(/(^|\/)gitignore$/, '$1.gitignore');
|
|
28
|
+
const outputPath = path_1.default.join(outputDir, outputName);
|
|
29
|
+
if (isEjs) {
|
|
30
|
+
await renderTemplateFile(filePath, outputPath, data);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
|
|
34
|
+
await fs_extra_1.default.copy(filePath, outputPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function getAllFiles(dir) {
|
|
39
|
+
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
40
|
+
const files = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
files.push(...(await getAllFiles(fullPath)));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
files.push(fullPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ValidationResult } from '../types';
|
|
2
|
+
export declare function isValidProjectName(name: string): ValidationResult;
|
|
3
|
+
export declare function isValidServiceName(name: string, usedNames: string[]): ValidationResult;
|
|
4
|
+
export declare function isValidPort(port: number, usedPorts: number[]): ValidationResult;
|
|
5
|
+
export declare function generatePassword(length?: number): string;
|
|
6
|
+
export declare function generateJwtSecret(length?: number): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isValidProjectName = isValidProjectName;
|
|
7
|
+
exports.isValidServiceName = isValidServiceName;
|
|
8
|
+
exports.isValidPort = isValidPort;
|
|
9
|
+
exports.generatePassword = generatePassword;
|
|
10
|
+
exports.generateJwtSecret = generateJwtSecret;
|
|
11
|
+
const validate_npm_package_name_1 = __importDefault(require("validate-npm-package-name"));
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
14
|
+
const RESERVED_SERVICE_NAMES = ['backend', 'db', 'pgadmin', 'adminer'];
|
|
15
|
+
function isValidProjectName(name) {
|
|
16
|
+
if (!name)
|
|
17
|
+
return 'Project name is required';
|
|
18
|
+
const result = (0, validate_npm_package_name_1.default)(name);
|
|
19
|
+
if (!result.validForNewPackages) {
|
|
20
|
+
const errors = [...(result.errors || []), ...(result.warnings || [])];
|
|
21
|
+
return errors[0] || 'Invalid project name';
|
|
22
|
+
}
|
|
23
|
+
if (!KEBAB_CASE_REGEX.test(name)) {
|
|
24
|
+
return 'Name must be in kebab-case (e.g. my-project)';
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function isValidServiceName(name, usedNames) {
|
|
29
|
+
if (!name)
|
|
30
|
+
return 'Service name is required';
|
|
31
|
+
if (!KEBAB_CASE_REGEX.test(name)) {
|
|
32
|
+
return 'Name must be in kebab-case (e.g. my-frontend)';
|
|
33
|
+
}
|
|
34
|
+
if (RESERVED_SERVICE_NAMES.includes(name)) {
|
|
35
|
+
return `Name "${name}" is reserved`;
|
|
36
|
+
}
|
|
37
|
+
if (usedNames.includes(name)) {
|
|
38
|
+
return `Name "${name}" is already taken`;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function isValidPort(port, usedPorts) {
|
|
43
|
+
if (!Number.isInteger(port))
|
|
44
|
+
return 'Port must be an integer';
|
|
45
|
+
if (port < 1024 || port > 65535)
|
|
46
|
+
return 'Port must be between 1024 and 65535';
|
|
47
|
+
if (port === 5432)
|
|
48
|
+
return 'Port 5432 is reserved for PostgreSQL';
|
|
49
|
+
if (usedPorts.includes(port)) {
|
|
50
|
+
return `Port ${port} is already in use`;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function generatePassword(length = 16) {
|
|
55
|
+
return crypto_1.default.randomBytes(length).toString('base64url').slice(0, length);
|
|
56
|
+
}
|
|
57
|
+
function generateJwtSecret(length = 32) {
|
|
58
|
+
return crypto_1.default.randomBytes(length).toString('hex');
|
|
59
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "boiling-fullstack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to scaffold fullstack projects with Docker, NestJS backend, and Vue/Nuxt frontends",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"boiling": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "ts-node src/index.ts",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["cli", "scaffold", "fullstack", "docker", "nuxt", "vue", "nestjs"],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@clack/prompts": "^0.7.0",
|
|
24
|
+
"chalk": "^4.1.2",
|
|
25
|
+
"commander": "^12.1.0",
|
|
26
|
+
"ejs": "^3.1.10",
|
|
27
|
+
"execa": "^5.1.1",
|
|
28
|
+
"fs-extra": "^11.2.0",
|
|
29
|
+
"picocolors": "^1.1.1",
|
|
30
|
+
"validate-npm-package-name": "^6.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/ejs": "^3.1.5",
|
|
34
|
+
"@types/fs-extra": "^11.0.4",
|
|
35
|
+
"@types/node": "^22.10.0",
|
|
36
|
+
"@types/validate-npm-package-name": "^4.0.2",
|
|
37
|
+
"ts-node": "^10.9.2",
|
|
38
|
+
"typescript": "^5.7.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
parser: '@typescript-eslint/parser',
|
|
3
|
+
parserOptions: {
|
|
4
|
+
project: 'tsconfig.json',
|
|
5
|
+
tsconfigRootDir: __dirname,
|
|
6
|
+
sourceType: 'module',
|
|
7
|
+
},
|
|
8
|
+
plugins: ['@typescript-eslint/eslint-plugin'],
|
|
9
|
+
extends: [
|
|
10
|
+
'plugin:@typescript-eslint/recommended',
|
|
11
|
+
],
|
|
12
|
+
root: true,
|
|
13
|
+
env: {
|
|
14
|
+
node: true,
|
|
15
|
+
jest: true,
|
|
16
|
+
},
|
|
17
|
+
ignorePatterns: ['.eslintrc.cjs'],
|
|
18
|
+
rules: {},
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Stage 1: Development
|
|
2
|
+
FROM node:20-alpine AS development
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm install
|
|
8
|
+
|
|
9
|
+
COPY . .
|
|
10
|
+
|
|
11
|
+
USER node
|
|
12
|
+
|
|
13
|
+
EXPOSE 3001
|
|
14
|
+
|
|
15
|
+
CMD ["npm", "run", "start:dev"]
|
|
16
|
+
|
|
17
|
+
# Stage 2: Build
|
|
18
|
+
FROM node:20-alpine AS build
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
COPY package*.json ./
|
|
23
|
+
RUN npm install
|
|
24
|
+
|
|
25
|
+
COPY . .
|
|
26
|
+
RUN npm run build
|
|
27
|
+
|
|
28
|
+
# Stage 3: Production
|
|
29
|
+
FROM node:20-alpine AS production
|
|
30
|
+
|
|
31
|
+
WORKDIR /app
|
|
32
|
+
|
|
33
|
+
COPY --from=build /app/dist ./dist
|
|
34
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
35
|
+
COPY --from=build /app/package*.json ./
|
|
36
|
+
|
|
37
|
+
USER node
|
|
38
|
+
|
|
39
|
+
EXPOSE 3001
|
|
40
|
+
|
|
41
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
42
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
|
|
43
|
+
|
|
44
|
+
CMD ["node", "dist/main"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= projectName %>-backend",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "nest build",
|
|
7
|
+
"start": "nest start",
|
|
8
|
+
"start:dev": "nest start --watch",
|
|
9
|
+
"start:debug": "nest start --debug --watch",
|
|
10
|
+
"start:prod": "node dist/main",
|
|
11
|
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
|
12
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
13
|
+
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/config/data-source.ts",
|
|
14
|
+
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/data-source.ts",
|
|
15
|
+
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/data-source.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@nestjs/common": "^10.4.0",
|
|
19
|
+
"@nestjs/config": "^3.3.0",
|
|
20
|
+
"@nestjs/core": "^10.4.0",
|
|
21
|
+
"@nestjs/jwt": "^10.2.0",
|
|
22
|
+
"@nestjs/passport": "^10.0.0",
|
|
23
|
+
"@nestjs/platform-express": "^10.4.0",
|
|
24
|
+
"@nestjs/typeorm": "^10.0.0",
|
|
25
|
+
"bcrypt": "^5.1.1",
|
|
26
|
+
"class-transformer": "^0.5.1",
|
|
27
|
+
"class-validator": "^0.14.1",
|
|
28
|
+
"passport": "^0.7.0",
|
|
29
|
+
"passport-jwt": "^4.0.1",
|
|
30
|
+
"pg": "^8.13.0",
|
|
31
|
+
"reflect-metadata": "^0.2.2",
|
|
32
|
+
"rxjs": "^7.8.0",
|
|
33
|
+
"typeorm": "^0.3.20"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@nestjs/cli": "^10.4.0",
|
|
37
|
+
"@nestjs/schematics": "^10.2.0",
|
|
38
|
+
"@types/bcrypt": "^5.0.2",
|
|
39
|
+
"@types/express": "^5.0.0",
|
|
40
|
+
"@types/node": "^22.10.0",
|
|
41
|
+
"@types/passport-jwt": "^4.0.1",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
43
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
44
|
+
"eslint": "^8.57.0",
|
|
45
|
+
"prettier": "^3.4.0",
|
|
46
|
+
"source-map-support": "^0.5.21",
|
|
47
|
+
"ts-loader": "^9.5.0",
|
|
48
|
+
"ts-node": "^10.9.2",
|
|
49
|
+
"tsconfig-paths": "^4.2.0",
|
|
50
|
+
"typescript": "^5.7.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { AppService } from './app.service';
|
|
3
|
+
|
|
4
|
+
@Controller()
|
|
5
|
+
export class AppController {
|
|
6
|
+
constructor(private readonly appService: AppService) {}
|
|
7
|
+
|
|
8
|
+
@Get()
|
|
9
|
+
getStatus() {
|
|
10
|
+
return { status: 'ok' };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Get('health')
|
|
14
|
+
getHealth() {
|
|
15
|
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
4
|
+
import { AppController } from './app.controller';
|
|
5
|
+
import { AppService } from './app.service';
|
|
6
|
+
import { AuthModule } from './auth/auth.module';
|
|
7
|
+
import { typeOrmConfig } from './config/typeorm.config';
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [
|
|
11
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
12
|
+
TypeOrmModule.forRootAsync({
|
|
13
|
+
useFactory: typeOrmConfig,
|
|
14
|
+
}),
|
|
15
|
+
AuthModule,
|
|
16
|
+
],
|
|
17
|
+
controllers: [AppController],
|
|
18
|
+
providers: [AppService],
|
|
19
|
+
})
|
|
20
|
+
export class AppModule {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
2
|
+
import { AuthService } from './auth.service';
|
|
3
|
+
import { RegisterDto } from './dto/register.dto';
|
|
4
|
+
import { LoginDto } from './dto/login.dto';
|
|
5
|
+
|
|
6
|
+
@Controller('auth')
|
|
7
|
+
export class AuthController {
|
|
8
|
+
constructor(private readonly authService: AuthService) {}
|
|
9
|
+
|
|
10
|
+
@Post('register')
|
|
11
|
+
register(@Body() registerDto: RegisterDto) {
|
|
12
|
+
return this.authService.register(registerDto);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Post('login')
|
|
16
|
+
login(@Body() loginDto: LoginDto) {
|
|
17
|
+
return this.authService.login(loginDto);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
3
|
+
import { PassportModule } from '@nestjs/passport';
|
|
4
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
5
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
6
|
+
import { AuthController } from './auth.controller';
|
|
7
|
+
import { AuthService } from './auth.service';
|
|
8
|
+
import { JwtStrategy } from './jwt.strategy';
|
|
9
|
+
import { User } from './entities/user.entity';
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
imports: [
|
|
13
|
+
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
14
|
+
JwtModule.registerAsync({
|
|
15
|
+
imports: [ConfigModule],
|
|
16
|
+
inject: [ConfigService],
|
|
17
|
+
useFactory: (configService: ConfigService) => ({
|
|
18
|
+
secret: configService.get<string>('JWT_SECRET'),
|
|
19
|
+
signOptions: { expiresIn: '1d' },
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
TypeOrmModule.forFeature([User]),
|
|
23
|
+
],
|
|
24
|
+
controllers: [AuthController],
|
|
25
|
+
providers: [AuthService, JwtStrategy],
|
|
26
|
+
exports: [AuthService],
|
|
27
|
+
})
|
|
28
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
|
|
2
|
+
import { JwtService } from '@nestjs/jwt';
|
|
3
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
4
|
+
import { Repository } from 'typeorm';
|
|
5
|
+
import * as bcrypt from 'bcrypt';
|
|
6
|
+
import { User } from './entities/user.entity';
|
|
7
|
+
import { RegisterDto } from './dto/register.dto';
|
|
8
|
+
import { LoginDto } from './dto/login.dto';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class AuthService {
|
|
12
|
+
constructor(
|
|
13
|
+
@InjectRepository(User)
|
|
14
|
+
private readonly userRepository: Repository<User>,
|
|
15
|
+
private readonly jwtService: JwtService,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async register(registerDto: RegisterDto) {
|
|
19
|
+
const { email, password } = registerDto;
|
|
20
|
+
|
|
21
|
+
const existingUser = await this.userRepository.findOne({ where: { email } });
|
|
22
|
+
if (existingUser) {
|
|
23
|
+
throw new ConflictException('Email already exists');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
27
|
+
const user = this.userRepository.create({ email, password: hashedPassword });
|
|
28
|
+
await this.userRepository.save(user);
|
|
29
|
+
|
|
30
|
+
const payload = { sub: user.id, email: user.email };
|
|
31
|
+
return { accessToken: this.jwtService.sign(payload) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async login(loginDto: LoginDto) {
|
|
35
|
+
const { email, password } = loginDto;
|
|
36
|
+
|
|
37
|
+
const user = await this.userRepository.findOne({ where: { email } });
|
|
38
|
+
if (!user) {
|
|
39
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isPasswordValid = await bcrypt.compare(password, user.password);
|
|
43
|
+
if (!isPasswordValid) {
|
|
44
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const payload = { sub: user.id, email: user.email };
|
|
48
|
+
return { accessToken: this.jwtService.sign(payload) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Entity,
|
|
3
|
+
PrimaryGeneratedColumn,
|
|
4
|
+
Column,
|
|
5
|
+
CreateDateColumn,
|
|
6
|
+
UpdateDateColumn,
|
|
7
|
+
} from 'typeorm';
|
|
8
|
+
|
|
9
|
+
@Entity('users')
|
|
10
|
+
export class User {
|
|
11
|
+
@PrimaryGeneratedColumn()
|
|
12
|
+
id: number;
|
|
13
|
+
|
|
14
|
+
@Column({ unique: true })
|
|
15
|
+
email: string;
|
|
16
|
+
|
|
17
|
+
@Column()
|
|
18
|
+
password: string;
|
|
19
|
+
|
|
20
|
+
@CreateDateColumn()
|
|
21
|
+
createdAt: Date;
|
|
22
|
+
|
|
23
|
+
@UpdateDateColumn()
|
|
24
|
+
updatedAt: Date;
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
3
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
4
|
+
import { ConfigService } from '@nestjs/config';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
8
|
+
constructor(configService: ConfigService) {
|
|
9
|
+
super({
|
|
10
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
11
|
+
ignoreExpiration: false,
|
|
12
|
+
secretOrKey: configService.get<string>('JWT_SECRET'),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
validate(payload: { sub: number; email: string }) {
|
|
17
|
+
return { id: payload.sub, email: payload.email };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DataSource } from 'typeorm';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export default new DataSource({
|
|
5
|
+
type: 'postgres',
|
|
6
|
+
url: process.env.DATABASE_URL,
|
|
7
|
+
entities: [join(__dirname, '..', '**', '*.entity.{ts,js}')],
|
|
8
|
+
migrations: [join(__dirname, '..', 'migrations', '*.{ts,js}')],
|
|
9
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NestFactory } from '@nestjs/core';
|
|
2
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
3
|
+
import { AppModule } from './app.module';
|
|
4
|
+
|
|
5
|
+
async function bootstrap() {
|
|
6
|
+
const app = await NestFactory.create(AppModule);
|
|
7
|
+
|
|
8
|
+
app.useGlobalPipes(
|
|
9
|
+
new ValidationPipe({
|
|
10
|
+
whitelist: true,
|
|
11
|
+
forbidNonWhitelisted: true,
|
|
12
|
+
transform: true,
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
app.enableCors();
|
|
17
|
+
|
|
18
|
+
await app.listen(3001);
|
|
19
|
+
}
|
|
20
|
+
bootstrap();
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"removeComments": true,
|
|
6
|
+
"emitDecoratorMetadata": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"target": "ES2021",
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"baseUrl": "./",
|
|
13
|
+
"incremental": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strictNullChecks": false,
|
|
16
|
+
"noImplicitAny": false,
|
|
17
|
+
"strictBindCallApply": false,
|
|
18
|
+
"forceConsistentCasingInFileNames": false,
|
|
19
|
+
"noFallthroughCasesInSwitch": false
|
|
20
|
+
}
|
|
21
|
+
}
|