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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +313 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +33 -0
  7. package/dist/scaffolder.d.ts +2 -0
  8. package/dist/scaffolder.js +89 -0
  9. package/dist/types.d.ts +28 -0
  10. package/dist/types.js +2 -0
  11. package/dist/utils/shell.d.ts +6 -0
  12. package/dist/utils/shell.js +53 -0
  13. package/dist/utils/template.d.ts +2 -0
  14. package/dist/utils/template.js +51 -0
  15. package/dist/utils/validation.d.ts +6 -0
  16. package/dist/utils/validation.js +59 -0
  17. package/package.json +40 -0
  18. package/templates/backend/nestjs/.dockerignore +3 -0
  19. package/templates/backend/nestjs/.eslintrc.cjs.ejs +19 -0
  20. package/templates/backend/nestjs/.prettierrc +6 -0
  21. package/templates/backend/nestjs/Dockerfile +44 -0
  22. package/templates/backend/nestjs/nest-cli.json +8 -0
  23. package/templates/backend/nestjs/package.json.ejs +52 -0
  24. package/templates/backend/nestjs/src/app.controller.ts +17 -0
  25. package/templates/backend/nestjs/src/app.module.ts.ejs +20 -0
  26. package/templates/backend/nestjs/src/app.service.ts +4 -0
  27. package/templates/backend/nestjs/src/auth/auth.controller.ts +19 -0
  28. package/templates/backend/nestjs/src/auth/auth.guard.ts +5 -0
  29. package/templates/backend/nestjs/src/auth/auth.module.ts +28 -0
  30. package/templates/backend/nestjs/src/auth/auth.service.ts +50 -0
  31. package/templates/backend/nestjs/src/auth/dto/login.dto.ts +10 -0
  32. package/templates/backend/nestjs/src/auth/dto/register.dto.ts +10 -0
  33. package/templates/backend/nestjs/src/auth/entities/user.entity.ts +25 -0
  34. package/templates/backend/nestjs/src/auth/jwt.strategy.ts +19 -0
  35. package/templates/backend/nestjs/src/config/data-source.ts +9 -0
  36. package/templates/backend/nestjs/src/config/typeorm.config.ts.ejs +8 -0
  37. package/templates/backend/nestjs/src/main.ts.ejs +20 -0
  38. package/templates/backend/nestjs/src/migrations/.gitkeep +0 -0
  39. package/templates/backend/nestjs/tsconfig.build.json +4 -0
  40. package/templates/backend/nestjs/tsconfig.json +21 -0
  41. package/templates/frontend/nuxt/.dockerignore +5 -0
  42. package/templates/frontend/nuxt/.eslintrc.cjs.ejs +5 -0
  43. package/templates/frontend/nuxt/.prettierrc +6 -0
  44. package/templates/frontend/nuxt/Dockerfile +38 -0
  45. package/templates/frontend/nuxt/app/app.vue +3 -0
  46. package/templates/frontend/nuxt/app/pages/index.vue.ejs +25 -0
  47. package/templates/frontend/nuxt/nuxt.config.ts.ejs +19 -0
  48. package/templates/frontend/nuxt/package.json.ejs +25 -0
  49. package/templates/frontend/nuxt/tsconfig.json +3 -0
  50. package/templates/frontend/vue/.dockerignore +3 -0
  51. package/templates/frontend/vue/.eslintrc.cjs.ejs +14 -0
  52. package/templates/frontend/vue/.prettierrc +6 -0
  53. package/templates/frontend/vue/Dockerfile +33 -0
  54. package/templates/frontend/vue/index.html.ejs +13 -0
  55. package/templates/frontend/vue/package.json.ejs +28 -0
  56. package/templates/frontend/vue/src/App.vue.ejs +22 -0
  57. package/templates/frontend/vue/src/main.ts +4 -0
  58. package/templates/frontend/vue/src/vite-env.d.ts +1 -0
  59. package/templates/frontend/vue/tsconfig.json +20 -0
  60. package/templates/frontend/vue/vite.config.ts.ejs +14 -0
  61. package/templates/root/.env.ejs +24 -0
  62. package/templates/root/.env.example.ejs +24 -0
  63. package/templates/root/.env.production.ejs +17 -0
  64. package/templates/root/Makefile.ejs +116 -0
  65. package/templates/root/README.md.ejs +158 -0
  66. package/templates/root/docker-compose.prod.yml.ejs +45 -0
  67. package/templates/root/docker-compose.yml.ejs +77 -0
  68. 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,2 @@
1
+ export declare function renderTemplateFile(templatePath: string, outputPath: string, data: Record<string, unknown>): Promise<void>;
2
+ export declare function copyAndRenderDir(templateDir: string, outputDir: string, data: Record<string, unknown>): Promise<void>;
@@ -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,3 @@
1
+ node_modules
2
+ dist
3
+ .git
@@ -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,6 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "printWidth": 100
6
+ }
@@ -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,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
@@ -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,4 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class AppService {}
@@ -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,5 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard } from '@nestjs/passport';
3
+
4
+ @Injectable()
5
+ export class JwtAuthGuard extends AuthGuard('jwt') {}
@@ -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,10 @@
1
+ import { IsEmail, IsString, MinLength } from 'class-validator';
2
+
3
+ export class LoginDto {
4
+ @IsEmail()
5
+ email: string;
6
+
7
+ @IsString()
8
+ @MinLength(8)
9
+ password: string;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { IsEmail, IsString, MinLength } from 'class-validator';
2
+
3
+ export class RegisterDto {
4
+ @IsEmail()
5
+ email: string;
6
+
7
+ @IsString()
8
+ @MinLength(8)
9
+ password: string;
10
+ }
@@ -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,8 @@
1
+ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
2
+
3
+ export const typeOrmConfig = (): TypeOrmModuleOptions => ({
4
+ type: 'postgres',
5
+ url: process.env.DATABASE_URL,
6
+ autoLoadEntities: true,
7
+ synchronize: process.env.NODE_ENV !== 'production',
8
+ });
@@ -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();
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ .nuxt
3
+ .output
4
+ dist
5
+ .git
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['@nuxt/eslint-config'],
4
+ rules: {},
5
+ }